#!/usr/bin/python

'''
CloudFiler.py

Main GUI script.  For a description of what CloudFiler does see:

http://www.nuvovis.com/cloud-filer-backup-tool.html

The icons used in Cloud Filer are open source and from the following sources:

Page-world, Film, Lock, Package, Image, Word, Excel & Powerpoint from
    http://findicons.com/pack/1588/farm_fresh_web by FatCow Web Hosting
    (http://creativecommons.org/licenses/by/3.0/)

Others from http://openiconlibrary.sourceforge.net/
    (Either public domain or http://creativecommons.org/licenses/by-sa/1.0/)


Developer reminder about Unicode:
You always get type str in python 2 for strings by default see Unicode
and Bytestrings in http://www.b-list.org/weblog/2007/nov/10/unicode/

Extract from https://docs.python.org/2.7/howto/unicode.html:
> Software should only work with Unicode strings internally, converting to a
> particular encoding on output.
>
> Include characters > 127 and, even better, characters > 255 in your test
> data.

'''

import wx
import sys
import os
import tempfile
import time
import re
import fnmatch
import datetime
import encodings.hex_codec
import encodings.ascii
import encodings.utf_8
import encodings.idna
import threading
import imp
import passlib.handlers.pbkdf2
import httplib
import ctypes

from boto.s3.connection import S3Connection
from boto.exception import S3ResponseError
from boto.exception import S3CreateError
from boto.s3.key import Key
from time import sleep
from itertools import chain
from ast import literal_eval
from passlib.hash import pbkdf2_sha256
from base64 import b64decode
from base64 import b64encode
from Crypto.Cipher import AES
from os import urandom

import Licensing
import AppConfig

#gbl_ignore_pattern = '.*'
ID_LOCAL_DIR_COMBO = 100
ID_REMOTE_DIR_COMBO = 101
ID_REMOTE_CONNECT = 102
ID_CLOUD_COMBO = 103
ID_SKIP_BUTTON = 104
ID_CLOUD_ACTION_CANCEL = 105
ID_NEW_CLOUD = 106
ID_DELETE_CLOUD = 107
ID_OPEN_HELP = 108
ID_OPEN_HELP_ABOUT = 109
ID_LOCAL_UPLOAD = 110
ID_REMOTE_DOWNLOAD = 111
ID_CLOUD_ENCRYPT = 112
ID_OPEN_HELP_LICENSING = 113

ID_EVT_ACTION = 108
def EVT_ACTION(win, func):
    """Define Action Event."""
    win.Connect(-1, -1, ID_EVT_ACTION, func)

ID_OS_LINUX = 1
ID_OS_MACOS = 2
ID_OS_WINDOWS = 3

def DEBUG(msg):
# Uncomment the following line for debug output
#    print msg.encode('utf-8', errors='replace')
    pass

def ensure_unicode(x):
    if not isinstance(x, unicode):
        return unicode(x.decode('utf-8'))
    else:
        return x

def ensure_ascii(x):
    if not isinstance(x, str):
        return str(x.encode('ascii'))
    else:
        return x

def filename_from_path(path):
    lastSlashPos = path.rfind('/')
    if lastSlashPos >= 0:
        return path[lastSlashPos+1:]
    else:
        return path


SORT_BY_FILENAME = 0
SORT_BY_FILESIZE = 1
SORT_BY_FILETYPE = 2
SORT_BY_MODIFIED = 3


def GetOS():
    if os.name == 'posix':
        # Either Linux or MacOS
        DEBUG('sys.platform=' + sys.platform)
        if sys.platform.lower().startswith('linux'):
            return ID_OS_LINUX
        else:
            return ID_OS_MACOS
    else:
        return ID_OS_WINDOWS


ACTION_DELETE_BUCKET = 1
ACTION_UPLOAD_FILE = 2
ACTION_DELETE_FILE = 3
ACTION_DELETE_FOLDER = 4
ACTION_DOWNLOAD_FILE = 5
ACTION_DELETE_CLOUD = 6
ACTION_SWITCH_ENCRYPTED = 7
ACTION_BETA_EXPIRED = 8
ACTION_NEW_VERSION = 9
ACTION_LICENSE_DETAILS = 10

# First icon IDs correspond with the FileListCtrl.il wx.ImageList
ICON_UP_DIR = 0
ICON_FOLDER = 1
ICON_DOC_PLAIN = 2
ICON_LOCK = 3
ICON_DOC_TEXT = 4
ICON_DOC_PDF = 5
ICON_DOC_IMAGE = 6
ICON_PACKAGE = 7
ICON_MUSIC = 8
ICON_FILM = 9
ICON_SUBTITLES = 10
ICON_CDROM = 11
ICON_SCRIPT_PYTHON = 12
ICON_DOC_WORD = 13
ICON_DOC_EXCEL = 14
ICON_DOC_POWERPOINT = 15
ICON_DOC_HTML = 16
ICON_SIGNATURE = 17
ICON_REMOTE_FOLDER = 18
ICON_CSS = 19
ICON_SCRIPT = 20
ICON_EXE = 21
ICON_LIB = 22
ICON_DLL = 23
ICON_SMALL_ARROW_DOWN = 24
ICON_SMALL_ARROW_UP = 25
ICON_GREEN_SHIELD = 26
ICON_RED_SHIELD = 27

ICON_WARNING = 100
ICON_UPLOAD_MERGE = 101
ICON_HELP_CONTENTS = 102
ICON_HELP_ABOUT = 103
ICON_APPLICATION = 104
USER_GUIDE_APPLICATION = 105
ICON_INFORMATION = 106
LICENSING_DLL = 107

CHUNK_SIZE = 8 * 1024 * 1024

FOLDER_CONTENTS_HEADER = 'Nuvovis Cloud Filer folder contents:'
CTA_ALTCHARS = './'
CF_VERSION_STRING = '1.1.1251'
CF_BUILD_NUMBER = 1251


def PadStr(clearText):
    padSize = AES.block_size - (len(clearText) % AES.block_size)
    DEBUG('padSize=' + str(padSize))
    return clearText + padSize * chr(padSize)


def UnpadStr(cypherText):
    padSize = ord(cypherText[-1])
    DEBUG('padSize=' + str(padSize))
    return cypherText[0:-padSize]


def EncryptStr(clearText, key):
    if not Licensing.EncryptionLicensed():
        return

    iv = urandom(AES.block_size)
    clearText = PadStr(clearText)
    crypto = AES.new(key, AES.MODE_CBC, iv)
    return b64encode(iv + crypto.encrypt(clearText))


def DecryptStr(cypherText, key):
    if not Licensing.EncryptionLicensed():
        return

    cypherText = b64decode(cypherText)
    iv = cypherText[0:AES.block_size]
    crypto = AES.new(key, AES.MODE_CBC, iv)
    return UnpadStr(crypto.decrypt(cypherText[AES.block_size:]))


def EncryptFile(in_file, out_file, key):
    bs = AES.block_size
    iv = urandom(bs)
    out_file.write(iv)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    finished = False
    while not finished:
        chunk = in_file.read(1024 * bs)
        if len(chunk) == 0 or len(chunk) % bs != 0:
            chunk = PadStr(chunk)
            finished = True
        out_file.write(cipher.encrypt(chunk))


def DecryptFile(in_file, out_file, key):
    bs = AES.block_size
    iv = in_file.read(bs)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    next_chunk = ''
    finished = False
    while not finished:
        chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
        if len(next_chunk) == 0:
            chunk = UnpadStr(chunk)
            finished = True
        out_file.write(chunk)


class ActionEvent(wx.PyEvent):
    """Simple event to carry arbitrary action data."""
    def __init__(self, data):
        wx.PyEvent.__init__(self)
        self.SetEventType(ID_EVT_ACTION)
        self.data = data

def percent_cb(complete, total):
    global gauge
    gauge.SetValue(complete)



class VersionCheckThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        DEBUG("Starting version check thread")
        try:
            conn = httplib.HTTPConnection("www.nuvovis.com")
            headers = {"User-Agent": "CloudFiler v" + CF_VERSION_STRING}
            conn.request("GET", "/cfver.txt", '', headers)
            httpResponse = conn.getresponse()
            if httpResponse.status == 200:
                if self.NewerVersion(httpResponse.read()):
                    self.FlagUpNewerVersion()
            else:
                DEBUG('Request for version failed: ' + str(httpResponse.status) + ' ' + httpResponse.reason)
        except:
            DEBUG('Request for version failed: exception')


    def NewerVersion(self, versionStr):
        DEBUG('NewerVersion: versionStr=' + versionStr)

        parts = versionStr.split('.')
        if len(parts) >= 3:
            majorVer = int(parts[0])
            minorVer = int(parts[1])
            buildNo = int(parts[2])
            DEBUG('NewerVersion: majorVer=' + str(majorVer))
            DEBUG('NewerVersion: minorVer=' + str(minorVer))
            DEBUG('NewerVersion: buildNo=' + str(buildNo))
            # Build number (SVN revision number) always increases so just use that
            if buildNo > CF_BUILD_NUMBER:
                DEBUG('NewerVersion: a newer version exists!')
                return True
            DEBUG('NewerVersion: this is the latest version')
        else:
            DEBUG('Invalid version string')

        return False


    def FlagUpNewerVersion(self):
        global gbl_newerVersionAvailable
        gbl_newerVersionAvailable = True


class DeleteThread (threading.Thread):

    def __init__(self, dlg, keys):
        threading.Thread.__init__(self)
        self.dlg = dlg
        self.keys = keys
        self.messages = ''


    def run(self):
        DEBUG("Waiting for dialog")
        while not self.dlg.IsModal():
            time.sleep(0.1)

        DEBUG("Starting delete thread")

        for key in self.keys:
            DEBUG('key.name=' + key.name)
            if key.name.endswith('/'):
                DEBUG('Ignoring dir')
                continue
            self.DeleteRemoteFile(key)
            global gauge
            gauge.Pulse()
            if self.dlg.GetCancelled():
                break

        # Close the upload dialog
        wx.PostEvent(self.dlg, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, wx.ID_CANCEL))
        

    def GetMessages(self):
        return self.messages


    def DeleteRemoteFile(self, key):
        DEBUG('DeleteRemoteFile: key=' + key.name)
        try:
            wx.PostEvent(self.dlg, ActionEvent(key.name))
            self.messages = self.messages + 'Deleted file ' + key.name + '\n'
            # Delete key
            key.delete()

        except S3ResponseError as ex:
            errorMessage = ex.error_message
            self.messages = self.messages + 'S3 Error: "' + errorMessage + '"' + '\n'

        except Exception as ex:
            errorMessage = str(ex)
            self.messages = self.messages + 'Error: ' + errorMessage + '\n'



class UploadThread (threading.Thread):

    def __init__(self, dlg, fileList):
        threading.Thread.__init__(self)
        self.dlg = dlg
        self.fileList = fileList
        self.bucket, self.keyName = self.fileList._remoteDirList.GetCurrentRemoteLocation()
        self.messages = unicode()


    def GetSomethingUploaded(self):
        return self.somethingUploaded


    def GetMessages(self):
        return self.messages


    def run(self):
        DEBUG("Waiting for dialog")
        while not self.dlg.IsModal():
            time.sleep(0.1)

        DEBUG("Starting upload thread")
        self.inError = False
        cancelled = False
        currentItem = self.fileList.GetFirstSelected()
        while currentItem != -1 and not cancelled:
            fileName = self.fileList.GetItem(currentItem, 0).GetText()
            fileType = self.fileList.GetItem(currentItem, 2).GetText()
            DEBUG('fileName=' + fileName)
            DEBUG('fileType=' + fileType)
            if fileType == 'Folder':
                DEBUG('it is a folder')
                cancelled, self.somethingUploaded = self.UploadFolder(self.bucket, self.keyName, fileName, self.dlg)
            else:
                DEBUG('it is a file')
                cancelled, self.somethingUploaded = self.UploadFile(self.bucket, self.keyName, fileName, self.dlg)

            currentItem = self.fileList.GetNextSelected(currentItem)
        # Close the upload dialog
        wx.PostEvent(self.dlg, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, wx.ID_CANCEL))


    def UploadFile(self, bucket, keyName, fileName, dlg):
        if self.dlg.GetCancelled():
            self.messages = self.messages + 'Upload cancelled' + '\n'
            return True, False

        DEBUG('UploadFile: fileName=' + fileName + '; keyName=' + keyName)
        cancelled = False
        somethingUploaded = False

        justFileName = filename_from_path(fileName)
        if justFileName.startswith('_CF'):
            self.inError = True
            self.errorMessage = 'Skipping file that looks like a CloudFiler encrypted file: ' + fileName
            DEBUG(self.errorMessage)
        else:
            tempFilePath = ''
            try:
                wx.PostEvent(self.dlg, ActionEvent(fileName))
                fileSize = os.path.getsize(fileName)
                DEBUG('fileSize=' + str(fileSize))
                global gauge
                gauge.SetRange(fileSize)

                global gbl_cloud_fp
                if len(self.fileList._remoteDirList._password) == 0:
                    gbl_cloud_fp = open(fileName, 'rb')
                else:
                    DEBUG('encrypt to temp file')
                    sec = os.path.getmtime(fileName)
                    timeStr = time.strftime('%Y-%m-%d %H:%M', time.localtime(sec))
                    gbl_cloud_fp = open(fileName, 'rb')
                    cryptoFileName = self.fileList._remoteDirList.GetAndRemoveExistingCryptoFileName(fileName)
                    if cryptoFileName is None:
                        cryptoFileName = self.fileList._remoteDirList.GetNewCryptoFileName()
                    tempFilePath = tempfile.gettempdir() + os.sep + cryptoFileName
                    tmpfp = open(tempFilePath, 'wb')
                    EncryptFile(gbl_cloud_fp, tmpfp, self.fileList._remoteDirList._encryptionKey)
                    gbl_cloud_fp.close()
                    tmpfp.close()
                    plaintextFileName = fileName
                    fileName = cryptoFileName
                    gbl_cloud_fp = open(tempFilePath, 'rb')

                DEBUG('bucket name=' + self.bucket.name)
                DEBUG('key name=' + keyName)
                DEBUG('pos=' + str(gbl_cloud_fp.tell()))
                k = Key(self.bucket)
                k.key = keyName + fileName.replace('\\', '/')
                DEBUG('key=' + k.key)
                k.set_contents_from_file(gbl_cloud_fp, cb=percent_cb, num_cb=fileSize)
                gbl_cloud_fp.close()

                if len(self.fileList._remoteDirList._password) > 0:
                    self.fileList._remoteDirList.AddedCryptoFile(cryptoFileName, plaintextFileName, str(fileSize), timeStr)
                    self.fileList._remoteDirList.UploadNewContentsInfo()

            except S3ResponseError as ex:
                self.inError = True
                if ex.error_message != None and len(ex.error_message) > 0:
                    msg = ex.error_message
                else:
                    msg = str(ex)
                self.errorMessage = 'S3 Error (cloud action): "' + msg + '"'
                DEBUG('S3 Cloud thread error: ' + self.errorMessage)

            except Exception as ex:
                self.inError = True
                self.errorMessage = 'Error (cloud action): ' + str(ex)
                DEBUG('Cloud thread error: ' + self.errorMessage)

            finally:
                if len(tempFilePath) > 0:
                    try:
                        os.unlink(tempFilePath)
                    except Exception as ex:
                        DEBUG('Unlink temp file error: ' + str(ex))

        if self.dlg.GetCancelled():
            self.messages = self.messages + 'Upload cancelled' + '\n'
            cancelled = True
        else:
            if self.inError:
                self.messages = self.messages + 'Upload of ' + fileName + ' failed - ' + self.errorMessage + '\n'
            else:
                if len(self.fileList._remoteDirList._password) > 0:
                    fileName = self.fileList._remoteDirList.GetCleartextFileName(fileName)
                self.messages = self.messages + 'Uploaded ' + fileName + '\n'
                somethingUploaded = True

        return cancelled, somethingUploaded



    def UploadFolder(self, bucket, keyName, folderName, dlg):
        if self.dlg.GetCancelled():
            self.messages = self.messages + 'Upload cancelled' + '\n'
            return True, False

        if not keyName.startswith('/'):
            keyName = '/' + keyName
        DEBUG('UploadFolder: folderName=' + folderName + '; keyName=' + keyName)

        lastSlashPos = folderName.rfind('/')
        if lastSlashPos >= 0:
            justfolderName = folderName[lastSlashPos+1:]
        else:
            justfolderName = folderName
        if justfolderName.startswith('_CF'):
            cancelled = False
            somethingUploaded = False
            self.messages = self.messages + 'Skipping folder that looks like a CloudFiler encrypted folder: ' + folderName + '\n'
        else:
            self.messages = self.messages + 'Uploading ' + folderName + ' to ' + bucket.name + keyName + '\n'

            originalLocalDir = ''
            originalRemoteDir = ''
            originalRemoteCryptoDir = ''
            # If we are in encrypted mode 
            if len(self.fileList._remoteDirList._password) > 0:

                # if there is a '/' char in the folderName
                firstSlashPos = folderName.find('/')
                assert (firstSlashPos != 0), "folderName started with slash"
                if firstSlashPos > 0:
                    # Get top folder name and remove top folder name from folderName
                    topFolderName = folderName[:firstSlashPos]
                    DEBUG('UploadFolder: topFolderName=' + topFolderName)
                    folderName = folderName[firstSlashPos+1:]
                    DEBUG('UploadFolder: folderName=' + folderName)
                else:
                    DEBUG('folderName is topFolderName')
                    topFolderName = folderName
                    DEBUG('UploadFolder: topFolderName=' + topFolderName)
                    folderName = '.'
                    DEBUG('UploadFolder: folderName=' + folderName)

                # If the remote top folder name does not exist i.e. is not in cryptoDirs
                if not self.fileList._remoteDirList.CryptoDirNameExists(topFolderName):
                    DEBUG('UploadFolder: create new remote crypto folder')
                    cryptoFolderName = self.fileList._remoteDirList.GetNewCryptoFolderName()
                    self.fileList._remoteDirList.CreateNewFolder(cryptoFolderName)
                    self.fileList._remoteDirList.AddedCryptoDir(cryptoFolderName, topFolderName)
                    self.fileList._remoteDirList.UploadNewContentsInfo()
                else:
                    DEBUG('UploadFolder: get existing remote crypto folder')
                    cryptoFolderName = self.fileList._remoteDirList.GetCryptoFolder(topFolderName)

                # move position to remote folder called folderName
                originalRemoteDir = self.fileList._remoteDirList._currentRemotePath
                originalRemoteCryptoDir = self.fileList._remoteDirList._currentRemoteCryptoPath
                self.fileList._remoteDirList._currentRemotePath = originalRemoteDir + '/' + topFolderName
                self.fileList._remoteDirList._currentRemoteCryptoPath = originalRemoteCryptoDir + '/' + cryptoFolderName
                DEBUG('UploadFolder: cd currentRemotePath=' + self.fileList._remoteDirList._currentRemotePath)
                DEBUG('UploadFolder: cd currentRemoteCryptoPath=' + self.fileList._remoteDirList._currentRemoteCryptoPath)
                bucket, keyName = self.fileList._remoteDirList.GetCurrentRemoteLocation()
                self.fileList._remoteDirList.GetEncryptedDirContents(bucket, keyName)

                # move local position to local folder called folderName
                originalLocalDir = os.getcwd()
                dirPath = originalLocalDir + '/' + topFolderName
                DEBUG('UploadFolder: lcd dirPath=' + dirPath)
                os.chdir(dirPath)

            cancelled = False
            somethingUploaded = False
            # Skip any folder we can't access
            try:
                files = sorted(os.listdir(ensure_unicode(folderName)))
                for i in files:
                    DEBUG('UploadFolder: i=' + i)
                    if folderName == '.':
                        filePath = i
                    else:
                        filePath = folderName + os.sep + i
                    if os.path.isdir(filePath):
                        cancelled, somethingUploaded = self.UploadFolder(bucket, keyName, filePath, dlg)
                    else:
                        cancelled, somethingUploaded = self.UploadFile(bucket, keyName, filePath, dlg)
                    if cancelled:
                        break

            except Exception as ex:
                self.messages = self.messages + 'Skipping folder that I failed to read\n'

            # Pop directory location back to parent
            if len(originalLocalDir) > 0:
                os.chdir(originalLocalDir)
            if len(originalRemoteDir) > 0:
                self.fileList._remoteDirList._currentRemotePath = originalRemoteDir
            if len(originalRemoteCryptoDir) > 0:
                self.fileList._remoteDirList._currentRemoteCryptoPath = originalRemoteCryptoDir

            # Update the crypto directory contents lists
            bucket, keyName = self.fileList._remoteDirList.GetCurrentRemoteLocation()
            # If we are in encrypted mode 
            if len(self.fileList._remoteDirList._password) > 0:
                self.fileList._remoteDirList.GetEncryptedDirContents(bucket, keyName)

        return cancelled, somethingUploaded



class DownloadThread (threading.Thread):

    def __init__(self, dlg, fileList):
        threading.Thread.__init__(self)
        self.dlg = dlg
        self.fileList = fileList
        self.bucket, self.keyName = self.fileList.GetCurrentRemoteLocation()
        if self.keyName.startswith('/'):
            self.keyName = self.keyName.lstrip('/')
        if len(self.keyName) > 0 and not self.keyName.endswith('/'):
            self.keyName = self.keyName + '/'
        self.messages = ''


    def GetSomethingDownloaded(self):
        return self.somethingDownloaded


    def GetMessages(self):
        return self.messages


    def run(self):
        DEBUG("Waiting for dialog")
        while not self.dlg.IsModal():
            time.sleep(0.1)

        DEBUG("Starting download thread")
        self.inError = False
        cancelled = False
        currentItem = self.fileList.GetFirstSelected()
        while currentItem != -1 and not cancelled:
            fileName = self.fileList.GetItem(currentItem, 0).GetText()
            fileType = self.fileList.GetItem(currentItem, 2).GetText()
            DEBUG('fileName=' + fileName)
            DEBUG('fileType=' + fileType)
            if fileType == 'Folder':
                DEBUG('it is a folder')
                cancelled, self.somethingDownloaded = self.DownloadFolder(self.bucket, self.keyName, fileName, self.dlg)
            else:
                DEBUG('it is a file')
                cancelled, self.somethingDownloaded = self.DownloadFile(self.bucket, self.keyName, fileName, self.dlg)

            currentItem = self.fileList.GetNextSelected(currentItem)
        # Close the upload dialog
        wx.PostEvent(self.dlg, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, wx.ID_CANCEL))


    def DownloadFile(self, bucket, keyName, fileName, dlg):
        DEBUG('DownloadFile: keyName = ' + keyName)
        DEBUG('DownloadFile: fileName = ' + fileName)

        try:
            cancelled = False
            somethingDownloaded = False
            # If not encrypted mode
            if len(self.fileList._password) == 0 or fileName.startswith('_CF'):
                downloadFileName = fileName
                decrypting = False
            else:
                downloadFileName = self.fileList.GetCryptoFileName(fileName)
                decrypting = True
            fileKey = keyName + downloadFileName.replace('\\', '/')
            DEBUG('DownloadFile: fileKey = ' + fileKey)
            k = self.bucket.get_key(fileKey)
            DEBUG('key=' + k.key)
            wx.PostEvent(self.dlg, ActionEvent(fileName))
            fileSize = k.size
            DEBUG('fileSize=' + str(fileSize))
            
            global gauge
            gauge.SetRange(fileSize)

            DEBUG('bucket name=' + self.bucket.name)
            DEBUG('key name=' + keyName)
            global gbl_cloud_fp
            gbl_cloud_fp = open(downloadFileName, 'wb')
            k.get_contents_to_file(gbl_cloud_fp, cb=percent_cb, num_cb=fileSize)
            gbl_cloud_fp.close()
            if decrypting:
                tmpfp = open(downloadFileName, 'rb') # Open the local obfuscated _CFLn file we have just downloaded
                fp = open(fileName, 'wb') # Open the file we really want to write to
                DecryptFile(tmpfp, fp, self.fileList._encryptionKey)
                fp.close()
                tmpfp.close()
                os.unlink(downloadFileName)

        except S3ResponseError as ex:
            self.inError = True
            if ex.error_message != None and len(ex.error_message) > 0:
                msg = ex.error_message
            else:
                msg = str(ex)
            self.errorMessage = 'S3 Error (cloud action): "' + msg + '"'
            DEBUG('S3 Cloud thread error: ' + self.errorMessage)

        except Exception as ex:
            self.inError = True
            self.errorMessage = 'Error (cloud action): ' + str(ex)
            DEBUG('Cloud thread error: ' + self.errorMessage)

        if self.dlg.GetCancelled():
            self.messages = self.messages + 'Download cancelled' + '\n'
            cancelled = True
        else:
            if self.inError:
                self.messages = self.messages + 'Download of ' + fileName + ' failed - ' + self.errorMessage + '\n'
            else:
                self.messages = self.messages + 'Downloaded ' + fileName + '\n'
                somethingDownloaded = True

        return cancelled, somethingDownloaded



    def DownloadFolder(self, bucket, keyName, folderName, dlg):
        DEBUG('DownloadFolder: keyName=' + keyName)
        DEBUG('DownloadFolder: folderName=' + folderName)
        self.messages = self.messages + 'Downloading ' + folderName + ' from ' + bucket.name + '/' + keyName + '\n'
        try:
            os.mkdir(folderName)

        except OSError as ex:
            # If this is an exception because the folder or file already exists
            if 'exists' in str(ex):
                self.messages = self.messages + 'Merging into existing folder: ' + folderName + '\n'

        # If we are in encrypted mode 
        if len(self.fileList._password) > 0:
            return self.DownloadFolderInCryptoMode(bucket, keyName, folderName, dlg)
        else:
            return self.DownloadFolderInCleartextMode(bucket, keyName, folderName, dlg)



    def DownloadFolderInCleartextMode(self, bucket, keyName, folderName, dlg):
        originalLocalDir = ''
        originalRemoteDir = ''

        # if there is a '/' char in the folderName
        firstSlashPos = folderName.find('/')
        assert (firstSlashPos != 0), "folderName started with slash"
        if firstSlashPos > 0:
            # Get top folder name and remove top folder name from folderName
            topFolderName = folderName[:firstSlashPos]
            DEBUG('DownloadFolder: topFolderName=' + topFolderName)
            folderName = folderName[firstSlashPos+1:]
            DEBUG('DownloadFolder: folderName=' + folderName)
        else:
            DEBUG('folderName is topFolderName')
            topFolderName = folderName
            DEBUG('DownloadFolder: topFolderName=' + topFolderName)
            folderName = '.'
            DEBUG('DownloadFolder: folderName=' + folderName)

        # move position to remote folder called folderName
        originalRemoteDir = self.fileList._currentRemotePath
        self.fileList._currentRemotePath = originalRemoteDir + '/' + topFolderName
        DEBUG('DownloadFolder: cd currentRemotePath=' + self.fileList._currentRemotePath)
        bucket, keyName = self.fileList.GetCurrentRemoteLocation()

        # move local position to local folder called folderName
        originalLocalDir = os.getcwd()
        dirPath = originalLocalDir + '/' + topFolderName
        DEBUG('DownloadFolder: lcd dirPath=' + dirPath)
        os.chdir(dirPath)

        cancelled = False
        somethingDownloaded = False

        DEBUG('listing files for download at: ' + keyName)
        files = bucket.list(keyName, '/')
        for fileEntry in files:
            if fileEntry.name == keyName + '.$folder$' or fileEntry.name == keyName:
                continue
            DEBUG('file or folder for download: ' + fileEntry.name)
            # If it's a folder
            if fileEntry.name.endswith('/'):
                justFolderName = filename_from_path(fileEntry.name.rstrip('/'))
                DEBUG('Folder for download: ' + justFolderName)
                cancelled, somethingDownloaded = self.DownloadFolder(bucket, keyName, justFolderName, dlg)
            else:
                justFileName = filename_from_path(fileEntry.name)
                DEBUG('File for download: ' + justFileName)
                cancelled, somethingDownloaded = self.DownloadFile(bucket, keyName, justFileName, dlg)
            if cancelled:
                break

        # Pop directory location back to parent
        if len(originalLocalDir) > 0:
            os.chdir(originalLocalDir)
        if len(originalRemoteDir) > 0:
            self.fileList._currentRemotePath = originalRemoteDir

        return cancelled, somethingDownloaded



    def DownloadFolderInCryptoMode(self, bucket, keyName, folderName, dlg):
        originalLocalDir = ''
        originalRemoteDir = ''
        originalRemoteCryptoDir = ''

        # if there is a '/' char in the folderName
        firstSlashPos = folderName.find('/')
        assert (firstSlashPos != 0), "folderName started with slash"
        if firstSlashPos > 0:
            # Get top folder name and remove top folder name from folderName
            topFolderName = folderName[:firstSlashPos]
            DEBUG('DownloadFolder: topFolderName=' + topFolderName)
            folderName = folderName[firstSlashPos+1:]
            DEBUG('DownloadFolder: folderName=' + folderName)
        else:
            DEBUG('folderName is topFolderName')
            topFolderName = folderName
            DEBUG('DownloadFolder: topFolderName=' + topFolderName)
            folderName = '.'
            DEBUG('DownloadFolder: folderName=' + folderName)

        # If the remote top folder name does not exist i.e. is not in cryptoDirs
        if not self.fileList.CryptoDirNameExists(topFolderName):
            raise Exception('CryptoDirNameExists is false for ' + topFolderName)
        else:
            DEBUG('DownloadFolder: get existing remote crypto folder')
            cryptoFolderName = self.fileList.GetCryptoFolder(topFolderName)

        # move position to remote folder called folderName
        originalRemoteDir = self.fileList._currentRemotePath
        originalRemoteCryptoDir = self.fileList._currentRemoteCryptoPath
        self.fileList._currentRemotePath = originalRemoteDir + '/' + topFolderName
        self.fileList._currentRemoteCryptoPath = originalRemoteCryptoDir + '/' + cryptoFolderName
        DEBUG('DownloadFolder: cd currentRemotePath=' + self.fileList._currentRemotePath)
        DEBUG('DownloadFolder: cd currentRemoteCryptoPath=' + self.fileList._currentRemoteCryptoPath)
        bucket, keyName = self.fileList.GetCurrentRemoteLocation()
        self.fileList.GetEncryptedDirContents(bucket, keyName)

        # move local position to local folder called folderName
        originalLocalDir = os.getcwd()
        dirPath = originalLocalDir + '/' + topFolderName
        DEBUG('DownloadFolder: lcd dirPath=' + dirPath)
        os.chdir(dirPath)

        cancelled = False
        somethingDownloaded = False
        for (s1, s2, s3, s4) in self.fileList.cryptoFiles:
            DEBUG('File for download: ' + s2)
            cancelled, somethingDownloaded = self.DownloadFile(bucket, keyName, s2, dlg)
            if cancelled:
                break

        for (s1, s2, s3, s4) in self.fileList.cryptoDirs:
            DEBUG('Folder for download: ' + s2)
            cancelled, somethingDownloaded = self.DownloadFolder(bucket, keyName, s2, dlg)
            if cancelled:
                break

        # Pop directory location back to parent
        if len(originalLocalDir) > 0:
            os.chdir(originalLocalDir)
        if len(originalRemoteDir) > 0:
            self.fileList._currentRemotePath = originalRemoteDir
        if len(originalRemoteCryptoDir) > 0:
            self.fileList._currentRemoteCryptoPath = originalRemoteCryptoDir

        # Update the crypto directory contents lists
        bucket, keyName = self.fileList.GetCurrentRemoteLocation()
        # If we are in encrypted mode 
        if len(self.fileList._password) > 0:
            self.fileList.GetEncryptedDirContents(bucket, keyName)

        return cancelled, somethingDownloaded


def set_main_dir():
    global gbl_main_dir
    if '__file__' not in globals():
        # Use next 2 lines and sys.executable if building with py2exe on Windows or freeze *nix
        DEBUG('os.path.dirname(sys.executable)=' + os.path.dirname(sys.executable))
        gbl_main_dir = os.path.dirname(sys.executable)
    else:
        # Use next 2 lines and sys.executable if running as an interpreted python script
        DEBUG('os.path.dirname(os.path.realpath(__file__))=' + os.path.dirname(os.path.realpath(__file__)))
        gbl_main_dir = os.path.dirname(os.path.realpath(__file__))

def get_main_dir():
    global gbl_main_dir
    return gbl_main_dir
    

def get_images_dir():
    return get_main_dir() + os.sep + 'images'


def debug_output(msg):
    sys.stderr.write(msg + '\n')


def GetFilePath(iconId):
    if iconId == LICENSING_DLL:
        path = os.path.realpath(get_main_dir())
        return path + os.sep + 'pgslicmt.dll'

    if iconId == USER_GUIDE_APPLICATION:
        path = os.path.realpath(get_main_dir())
        return path + os.sep + 'CloudFilerUserGuide.pdf'

    path = os.path.realpath(get_images_dir())
    
    if iconId == ICON_WARNING:
        return path + os.sep + 'pictogram-din-w000-general.png'
    elif iconId == ICON_INFORMATION:
        return path + os.sep + 'pictogram-din-m000-general.png'
    elif iconId == ICON_FOLDER:
        return path + os.sep + 'document-open-folder.png'
    elif iconId == ICON_REMOTE_FOLDER:
        return path + os.sep + 'document-open-remote.png'
    elif iconId == ICON_UP_DIR:
        return path + os.sep + 'go-up-5.png'
    elif iconId == ICON_DOC_PLAIN:
        return path + os.sep + 'application-x-zerosize.png'
    elif iconId == ICON_LOCK:
        return path + os.sep + 'lock.png'
    elif iconId == ICON_DOC_TEXT:
        return path + os.sep + 'text_align_left.png'
    elif iconId == ICON_DOC_PDF:
        return path + os.sep + 'application-pdf.png'
    elif iconId == ICON_DOC_IMAGE:
        return path + os.sep + 'image.png'
    elif iconId == ICON_PACKAGE:
        return path + os.sep + 'package.png'
    elif iconId == ICON_MUSIC:
        return path + os.sep + 'music.png'
    elif iconId == ICON_FILM:
        return path + os.sep + 'film.png'
    elif iconId == ICON_SUBTITLES:
        return path + os.sep + 'text-x-dtd.png'
    elif iconId == ICON_CDROM:
        return path + os.sep + 'cd.png'
    elif iconId == ICON_SCRIPT_PYTHON:
        return path + os.sep + 'text-x-python.png'
    elif iconId == ICON_DOC_WORD:
        return path + os.sep + 'word.png'
    elif iconId == ICON_DOC_EXCEL:
        return path + os.sep + 'excel.png'
    elif iconId == ICON_DOC_POWERPOINT:
        return path + os.sep + 'powerpoint.png'
    elif iconId == ICON_DOC_HTML:
        return path + os.sep + 'page_world.png'
    elif iconId == ICON_SIGNATURE:
        return path + os.sep + 'text_signature.png'
    elif iconId == ICON_UPLOAD_MERGE:
        return path + os.sep + 'go-next-2.png'
    elif iconId == ICON_HELP_CONTENTS:
        return path + os.sep + 'help-contents-5.png'
    elif iconId == ICON_HELP_ABOUT:
        return path + os.sep + 'help.png'
    elif iconId == ICON_CSS:
        return path + os.sep + 'css.png'
    elif iconId == ICON_SCRIPT:
        return path + os.sep + 'script_gear.png'
    elif iconId == ICON_EXE:
        return path + os.sep + 'cog.png'
    elif iconId == ICON_LIB:
        return path + os.sep + 'page_link.png'
    elif iconId == ICON_DLL:
        return path + os.sep + 'link.png'
    elif iconId == ICON_APPLICATION:
        return path + os.sep + 'app.ico'
    elif iconId == ICON_SMALL_ARROW_DOWN:
        return path + os.sep + 'bullet_arrow_down.png'
    elif iconId == ICON_SMALL_ARROW_UP:
        return path + os.sep + 'bullet_arrow_up.png'
    elif iconId == ICON_GREEN_SHIELD:
        return path + os.sep + 'shield_green.png'
    elif iconId == ICON_RED_SHIELD:
        return path + os.sep + 'shield_red.png'

    raise Exception('Unexpecetd icon id: ' + str(iconId))



class CloudActionDlg(wx.Dialog):

    def __init__(self, action, *args, **kwargs):
        super(CloudActionDlg, self).__init__(*args, **kwargs) 
        
        self.action = action
        self.InitUI()

        
    def InitUI(self):
        self.SetSize((400, 160))

        self.label1 = wx.StaticText(self, pos=(10, 15))

        global gauge
        gauge = wx.Gauge(self, size=(380, 20), pos=(10, 45))
        self.cloudActionCancelled = False

        cancelButton = wx.Button(self, id=ID_CLOUD_ACTION_CANCEL, label='Cancel', pos=(300, 90))
        cancelButton.SetFocus()

        cancelButton.Bind(wx.EVT_BUTTON, self.OnCancel)
        cancelButton.Bind(wx.EVT_BUTTON, self.OnCancel)
        
        EVT_ACTION(self, self.OnAction)


    def OnAction(self, e):
        self.SetActionMsg(e.data)


    def SetActionMsg(self, objectName):
        if len(objectName) > 56:
            objectName = objectName[:56] + '...'

        if self.action == ACTION_DELETE_BUCKET:
            msg = 'Deleting ' + objectName
        elif self.action == ACTION_UPLOAD_FILE:
            msg = 'Uploading ' + objectName
        elif self.action == ACTION_DOWNLOAD_FILE:
            msg = 'Downloading ' + objectName
        else:
            msg = 'Handling ' + objectName

        self.label1.SetLabel(msg)

        
    def OnCancel(self, e):        
        endId = e.GetId()
        if endId == ID_CLOUD_ACTION_CANCEL:
            DEBUG('cancel')
            self.cloudActionCancelled = True
            self.label1.SetLabel('Cancelling...')

            if self.action == ACTION_UPLOAD_FILE or self.action == ACTION_DOWNLOAD_FILE:
                done = False
                count = 100
                while not done and count > 0:
                    count = count - 1
                    global gbl_cloud_fp
                    try:
                        DEBUG('try closing fp')
                        gbl_cloud_fp.close()
                        done = True
                    except:
                        DEBUG('bombed')

        elif endId == wx.ID_CANCEL:
            self.Destroy()


    def GetCancelled(self):
        return self.cloudActionCancelled


class WarningDlg(wx.Dialog):

    def __init__(self, action, showApplyToAll, objectName, *args, **kwargs):
        super(WarningDlg, self).__init__(*args, **kwargs) 
        
        self.InitUI(action, showApplyToAll, objectName)

        
    def InitUI(self, action, showApplyToAll, objectName):
        if showApplyToAll:
            self.SetSize((400, 290))
        else:
            self.SetSize((400, 220))

        if len(objectName) > 32:
            objectName = objectName[:32] + '...'

        if action == ACTION_DELETE_BUCKET:
            title = 'Delete bucket?'
            msg = 'You are about to delete the bucket\n\n\t' + objectName + '\n\nand all its files.'
        elif action == ACTION_DELETE_FILE:
            title = 'Delete file?'
            msg = 'You are about to delete\n\n\t' + objectName
        elif action == ACTION_DELETE_FOLDER:
            title = 'Delete folder?'
            msg = 'You are about to delete\n\n\t' + objectName + '\n\nand all its files.'
        elif action == ACTION_DELETE_CLOUD:
            title = 'Delete cloud settings?'
            msg = 'You are about to delete\n\n\t' + objectName + '\n\nand all its settings.'
        else:
            raise Exception('Unexpected action')

        icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(GetFilePath(ICON_WARNING)), pos=(10, 15))

        label1 = wx.StaticText(self, label=title, pos=(70, 15))
        DEBUG(msg)
        label1.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))

        label2 = wx.StaticText(self, label=msg, pos=(70, 45), size=(380,100))

        if showApplyToAll:
            self.repeatCheckbox = wx.CheckBox(self, label='Apply this action to all', pos=(70, 165))

            okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(100, 220))
            skipButton = wx.Button(self, id=ID_SKIP_BUTTON, label='Skip', pos=(200, 220))
            cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 220))
        else:
            okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(100, 150))
            skipButton = wx.Button(self, id=ID_SKIP_BUTTON, label='Skip', pos=(200, 150))
            cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 150))

        cancelButton.SetFocus()

        okButton.Bind(wx.EVT_BUTTON, self.OnClose)
        skipButton.Bind(wx.EVT_BUTTON, self.OnClose)
        cancelButton.Bind(wx.EVT_BUTTON, self.OnClose)
        
        
    def OnClose(self, e):        
        self.endId = e.GetId()
        self.Destroy()


    def GetEndId(self):
        return self.endId


    def GetApplyAll(self):
        return self.repeatCheckbox.GetValue()



class InformationDlg(wx.Dialog):

    def __init__(self, action, warning, *args, **kwargs):
        super(InformationDlg, self).__init__(*args, **kwargs) 
        
        self.InitUI(action, warning)

        
    def InitUI(self, action, warning):
        self.SetSize((400, 220))

        if action == ACTION_SWITCH_ENCRYPTED:
            title = 'Cloud Data Encryption'
            msg = 'Note that you must remember your password!\n'
            msg = msg + 'Without your password there is no way to\n'
            msg = msg + 'recover your data.'
        elif action == ACTION_NEW_VERSION:
            title = 'New version available!'
            msg = 'Download a new version from:\n\n'
            msg = msg + '    http://www.nuvovis.com'
        elif action == ACTION_LICENSE_DETAILS:
            licensedAppName = Licensing.GetAppName()
            title = licensedAppName
            msg = 'Licensed to ' + Licensing.GetClientName() + '\n'
            msg = msg + 'License key: ' + Licensing.GetLicenseKey() + '\n'
            if (Licensing.IsPerpetual()):
                msg = msg + 'License never expires\n'
            else:
                msg = msg + 'License expires on ' + Licensing.GetExpiryDate() + '\n'
        else:
            raise Exception('Unexpected action')

        if warning:
            icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(GetFilePath(ICON_WARNING)), pos=(10, 15))
        else:
            icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(GetFilePath(ICON_INFORMATION)), pos=(10, 15))

        label1 = wx.StaticText(self, label=title, pos=(70, 15))
        DEBUG(msg)
        label1.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))

        label2 = wx.StaticText(self, label=msg, pos=(70, 45), size=(300, 75))

        if action == ACTION_SWITCH_ENCRYPTED or action == ACTION_NEW_VERSION:
            self.enoughAlreadyCheckbox = wx.CheckBox(self, label='No need to show this again', pos=(70, 120))

        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(300, 150))

        okButton.Bind(wx.EVT_BUTTON, self.OnClose)
        okButton.SetFocus()
        
        
    def OnClose(self, e):        
        self.EndModal(0)


    def GetEnoughAlready(self):
        return self.enoughAlreadyCheckbox.GetValue()



class NewBucketOrFolderDlg(wx.Dialog):

    def __init__(self, isBucket, *args, **kwargs):
        super(NewBucketOrFolderDlg, self).__init__(*args, **kwargs) 
        
        self.InitUI(isBucket)

        
    def InitUI(self, isBucket):
        self.SetSize((400, 130))

        if isBucket:
            labelText = 'Bucket name:'
        else:
            labelText = 'Folder name:'
        label1 = wx.StaticText(self, label=labelText, pos=(10, 15))
        self.newName = wx.TextCtrl(self, pos=(106, 11))
        self.newName.SetFocus()
        cw = self.newName.GetCharWidth()
        w,h = self.newName.GetSizeTuple()
        self.newName.SetSize((cw * 32, h))

        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(200, 65))
        okButton.Bind(wx.EVT_BUTTON, self.OnClose)
        cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 65))

    def OnClose(self, e):        
        # Validate fields
        newName = self.newName.GetValue()
        if newName.startswith('_CF'):
            dlg = wx.MessageDialog(None, 'Invalid name (cannot start with _CF)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(newName) < 1:
            dlg = wx.MessageDialog(None, 'Empty name', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        else:
            self.EndModal(wx.ID_OK)

    def GetName(self):
        return self.newName.GetValue()


class GetNewLicenseDlg(wx.Dialog):

    def __init__(self, licMsg, *args, **kwargs):
        super(GetNewLicenseDlg, self).__init__(*args, **kwargs) 
        
        self.InitUI(licMsg)

        
    def InitUI(self, licMsg):
        self.SetSize((400, 130))

        label0 = wx.StaticText(self, label=licMsg, pos=(10, 15))

        label1 = wx.StaticText(self, label='License Key:', pos=(10, 45))
        self.licenseKey = wx.TextCtrl(self, pos=(106, 41))
        self.licenseKey.SetFocus()
        cw = self.licenseKey.GetCharWidth()
        w,h = self.licenseKey.GetSizeTuple()
        self.licenseKey.SetSize((cw * 32, h))

        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(200, 65))
        okButton.Bind(wx.EVT_BUTTON, self.OnClose)
        cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 65))

    def OnClose(self, e):        
        # Validate fields
        licenseKey = self.licenseKey.GetValue()
        if len(licenseKey) < 1:
            dlg = wx.MessageDialog(None, 'Empty key', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        else:
            self.EndModal(wx.ID_OK)

    def GetKey(self):
        return ensure_ascii(self.licenseKey.GetValue().strip())



class RenameDlg(wx.Dialog):

    def __init__(self, oldName, *args, **kwargs):
        super(RenameDlg, self).__init__(*args, **kwargs) 
        
        self.InitUI(oldName)

        
    def InitUI(self, oldName):
        self.SetSize((400, 180))

        label1 = wx.StaticText(self, label='Old name:', pos=(10, 15))
        oldNameText = wx.StaticText(self, label=oldName, pos=(110, 15))

        label1 = wx.StaticText(self, label='New name:', pos=(10, 45))
        self.newName = wx.TextCtrl(self, pos=(106, 41))
        self.newName.SetValue(oldName)
        self.newName.SelectAll()
        self.newName.SetFocus()
        cw = self.newName.GetCharWidth()
        w,h = self.newName.GetSizeTuple()
        self.newName.SetSize((cw * 32, h))

        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(200, 85))
        cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 85))

    def GetNewName(self):
        return self.newName.GetValue()


class NewCloudDlg(wx.Dialog):

    def __init__(self, *args, **kwargs):
        super(NewCloudDlg, self).__init__(*args, **kwargs) 
        self.InitUI()
        
    def InitUI(self):
        self.SetSize((400, 180))

        label1 = wx.StaticText(self, label='Cloud Service:', pos=(10, 15))
        self.cloudService = wx.TextCtrl(self, pos=(130, 10))
        cw = self.cloudService.GetCharWidth()
        w,h = self.cloudService.GetSizeTuple()
        self.cloudService.SetSize((cw * 22, h))

        label2 = wx.StaticText(self, label='Cloud Host:', pos=(10, 45))
        self.cloudHost = wx.TextCtrl(self, pos=(130, 40))
        cw = self.cloudHost.GetCharWidth()
        w,h = self.cloudHost.GetSizeTuple()
        self.cloudHost.SetSize((cw * 32, h))

        label3 = wx.StaticText(self, label='Short Name:', pos=(10, 75))
        self.shortName = wx.TextCtrl(self, pos=(130, 70))
        cw = self.shortName.GetCharWidth()
        w,h = self.shortName.GetSizeTuple()
        self.shortName.SetSize((cw * 4, h))

        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(200, 105))
        okButton.Bind(wx.EVT_BUTTON, self.OnClose)
        cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(300, 105))


    def OnClose(self, e):        
        # Validate fields
        cloudName = self.cloudService.GetValue()
        cloudHost = self.cloudHost.GetValue()
        shortName = self.shortName.GetValue().upper()
        if '/' in cloudName or '\\' in cloudName or ':' in cloudName:
            dlg = wx.MessageDialog(None, 'Invalid cloud name (no slashes or colons)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(cloudName) < 1:
            dlg = wx.MessageDialog(None, 'Empty cloud name', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif '/' in cloudHost or '\\' in cloudHost or ':' in cloudHost or ' ' in cloudHost:
            dlg = wx.MessageDialog(None, 'Invalid cloud host (no slashes, spaces or colons)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(cloudHost) < 1:
            dlg = wx.MessageDialog(None, 'Empty cloud host', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(shortName) != 2 or not shortName.isalpha():
            dlg = wx.MessageDialog(None, 'Invalid short name (expect two alphabetic characters)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif AppConfig.HasSection(cloudName):
            dlg = wx.MessageDialog(None, 'Already have a cloud called ' + cloudName, 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif self.alreadyGotShortName(shortName):
            dlg = wx.MessageDialog(None, 'Already have a short name = ' + shortName, 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        else:
            self.EndModal(wx.ID_OK)
            

    def alreadyGotShortName(self, shortName):
        for section in AppConfig.GetSections():
            if section != 'History':
                if AppConfig.Get(section, 'ShortName') == shortName:
                    return True
        
        return False


    def GetCloudService(self):
        return self.cloudService.GetValue()


    def GetCloudHost(self):
        return self.cloudHost.GetValue()


    def GetShortName(self):
        return self.shortName.GetValue().upper()


class CloudConnectDlg(wx.Dialog):

    def __init__(self, *args, **kwargs):
        super(CloudConnectDlg, self).__init__(*args, **kwargs) 
            
        self.InitUI()
        

    def InitUI(self):
        self.SetSize((430, 415)) # Y size = OkButton YPos + 70

        # Cloud connection fields
        label1 = wx.StaticText(self, label='Cloud Service:', pos=(10, 15))
        self.cloudCombo = wx.ComboBox(self, ID_CLOUD_COMBO, style=wx.TE_PROCESS_ENTER, pos=(130, 10)) 
        delButton = wx.Button(self, id=ID_DELETE_CLOUD, label='Delete', pos=(330, 10))
        for section in AppConfig.GetSections():
            if section != 'History':
                self.cloudCombo.Append(section)
        self.cloudCombo.Append('New...')

        label2 = wx.StaticText(self, label='Cloud Host:', pos=(10, 45))
        label3 = wx.StaticText(self, label='Short Name:', pos=(10, 75))

        self.hostText = wx.StaticText(self, pos=(130, 45))
        self.rootText = wx.StaticText(self, pos=(130, 75))

        # Cloud authentication fields
        box1 = wx.StaticBox(self, label='User', pos=(5, 100), size=(410, 115))

        label4 = wx.StaticText(self, label='Access Key:', pos=(10, 125))
        label5 = wx.StaticText(self, label='Secret Key:', pos=(10, 155))

        self.accessKey = wx.TextCtrl(self, style=wx.TE_PASSWORD, pos=(130, 121))
        cw = self.accessKey.GetCharWidth()
        w,h = self.accessKey.GetSizeTuple()
        self.accessKey.SetSize((cw * 35, h))

        self.secretKey = wx.TextCtrl(self, style=wx.TE_PASSWORD, pos=(130, 151))
        cw = self.secretKey.GetCharWidth()
        w,h = self.secretKey.GetSizeTuple()
        self.secretKey.SetSize((cw * 35, h))

        self.storeKeys = wx.CheckBox(self, label='Save keys', pos=(130, 181))

        # Encryption fields
        box2 = wx.StaticBox(self, label='Security', pos=(5, 220), size=(410, 105))
        self.encrypt = wx.CheckBox(self, id=ID_CLOUD_ENCRYPT, label='Encrypt', pos=(130, 235))

        self.passwordLabel = wx.StaticText(self, label='Password:', pos=(10, 265))
        self.passwordLabel.Enable(False)

        self.password = wx.TextCtrl(self, style=wx.TE_PASSWORD, pos=(130, 261))
        self.password.Enable(False)
        cw = self.password.GetCharWidth()
        w,h = self.password.GetSizeTuple()
        self.password.SetSize((cw * 35, h))

        self.storePassword = wx.CheckBox(self, label='Save password', pos=(130, 291))
        self.storePassword.Enable(False)

        if not Licensing.EncryptionLicensed():
            self.encrypt.Enable(False)

        # Bottom buttons
        okButton = wx.Button(self, id=wx.ID_OK, label='Ok', pos=(230, 345))
        cancelButton = wx.Button(self, id=wx.ID_CANCEL, label='Cancel', pos=(330, 345))

        # Wire up events
        self.Bind(wx.EVT_COMBOBOX, self.OnChangeCloud, id=ID_CLOUD_COMBO)
        self.Bind(wx.EVT_BUTTON, self.OnNewCloud, id=ID_NEW_CLOUD)
        self.Bind(wx.EVT_BUTTON, self.OnDeleteCloud, id=ID_DELETE_CLOUD)
        self.Bind(wx.EVT_CHECKBOX, self.OnChangeEncrypt, id=ID_CLOUD_ENCRYPT)
        okButton.Bind(wx.EVT_BUTTON, self.OnClose)

        # Set the combobox selection
        selectedCloud = AppConfig.Get('History', 'LastCloud')
        try:
            sn = AppConfig.Get(selectedCloud, 'ShortName')
            if len(sn) == 0:
                raise Exception('That doesnt look any good to me')
        except:
            selectedCloud = AppConfig.GetSections()[0] # Didn't find that cloud so use the first
        self.SetCloudSelection(selectedCloud)

        okButton.SetFocus()


    def OnClose(self, e):        
        # Validate fields
        if self.encrypt.GetValue() and len(self.password.GetValue()) < 4:
            dlg = wx.MessageDialog(None, 'Invalid password (must be at least 4 characters long)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(self.accessKey.GetValue()) < 1:
            dlg = wx.MessageDialog(None, 'Invalid access key (must be at least 1 character long)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        elif len(self.secretKey.GetValue()) < 1:
            dlg = wx.MessageDialog(None, 'Invalid secret key (must be at least 1 character long)', 'Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
        else:
            self.EndModal(wx.ID_OK)


    def OnChangeEncrypt(self, e):
        if self.encrypt.GetValue() and Licensing.EncryptionLicensed():
            self.password.Enable(True)
            self.passwordLabel.Enable(True)
            self.storePassword.Enable(True)
            try:
                suppressWarn = AppConfig.Get('History', 'UserSupressedPasswordWarning')
            except:
                suppressWarn = 'N'
            if suppressWarn != 'Y':
                dlg = InformationDlg(ACTION_SWITCH_ENCRYPTED, True, self, wx.ID_ANY, 'Cloud Filer - Warning')
                dlg.ShowModal()
                if dlg.GetEnoughAlready():
                    AppConfig.Set('History', 'UserSupressedPasswordWarning', 'Y')
                    AppConfig.WriteSettings()
                dlg.Destroy()

        else:
            self.password.Enable(False)
            self.passwordLabel.Enable(False)
            self.storePassword.Enable(False)


    def SetCloudSelection(self, selection):
        self.cloudCombo.SetStringSelection(selection)

        try:
            cloudHost = AppConfig.Get(selection, 'Host')
        except:
            cloudHost = ''

        try:
            shortName = AppConfig.Get(selection, 'ShortName')
        except:
            shortName = ''

        try:
            saveKeys = AppConfig.Get(selection, 'SaveKeys')
            if saveKeys == 'Y':
                self.storeKeys.SetValue(True)
            else:
                self.storeKeys.SetValue(False)
        except:
            self.storeKeys.SetValue(False)

        try:
            encrypt = AppConfig.Get(selection, 'Encrypt')
            if encrypt == 'Y' and Licensing.EncryptionLicensed():
                self.encrypt.SetValue(True)
            else:
                self.encrypt.SetValue(False)
        except:
            self.encrypt.SetValue(False)
        if self.encrypt.GetValue():
            DEBUG('encrypt=Y')
            self.password.Enable(True)
            self.passwordLabel.Enable(True)
            self.storePassword.Enable(True)
        else:
            DEBUG('encrypt=N')
            self.password.Enable(False)
            self.passwordLabel.Enable(False)
            self.storePassword.Enable(False)

        try:
            storePassword = AppConfig.Get(selection, 'StorePassword')
            if storePassword == 'Y' and Licensing.EncryptionLicensed():
                self.storePassword.SetValue(True)
            else:
                self.storePassword.SetValue(False)
        except:
            self.storePassword.SetValue(False)
        if self.storePassword.GetValue():
            DEBUG('storePassword=Y')
        else:
            DEBUG('storePassword=N')

        self.hostText.ClearBackground()
        self.hostText.SetLabel(cloudHost)
        self.rootText.ClearBackground()
        self.rootText.SetLabel(shortName + ':')

        keyKey = 'nuvovis.com CloudFiler ' + AppConfig.Get(selection, 'ShortName')
        DEBUG('keyKey=' + keyKey)
        pw = AppConfig.SecureGet(keyKey, 'AK')
        DEBUG('AK=' + pw)
        self.accessKey.SetValue(pw)

        pw = AppConfig.SecureGet(keyKey, 'SK')
        DEBUG('SK=' + pw)
        self.secretKey.SetValue(pw)

        if Licensing.EncryptionLicensed():
            pw = AppConfig.SecureGet(keyKey, 'PWD')
        else:
            pw = ''
        DEBUG('PWD=' + pw)
        self.password.SetValue(pw)


    def SaveLastConnection(self):
        newSection = 'History'
        if not AppConfig.HasSection(newSection):
            AppConfig.AddSection(newSection)
        cloudService = self.cloudCombo.GetStringSelection()
        DEBUG('cloudService=' + cloudService)
        if self.storeKeys.IsChecked():
            saveKeys = 'Y'
        else:
            saveKeys = 'N'
        DEBUG('saveKeys=' + saveKeys)
        if self.encrypt.IsChecked():
            encrypt = 'Y'
        else:
            encrypt = 'N'
        DEBUG('encrypt=' + encrypt)
        if self.storePassword.IsChecked():
            storePassword = 'Y'
        else:
            storePassword = 'N'
        DEBUG('storePassword=' + storePassword)

        AppConfig.Set(newSection, 'LastCloud', cloudService)
        AppConfig.Set(cloudService, 'SaveKeys', saveKeys)
        AppConfig.Set(cloudService, 'Encrypt', encrypt)
        AppConfig.Set(cloudService, 'StorePassword', storePassword)
        AppConfig.WriteSettings()


    def PopupNewCloudDlg(self):
        dlg = NewCloudDlg(self, wx.ID_ANY, 'Add new cloud')
        if dlg.ShowModal() == wx.ID_OK:
            newSection = dlg.GetCloudService()
            AppConfig.AddSection(newSection)
            AppConfig.Set(newSection, 'Host', dlg.GetCloudHost())
            AppConfig.Set(newSection, 'ShortName', dlg.GetShortName())
            AppConfig.WriteSettings()
            self.cloudCombo.Insert(newSection, self.cloudCombo.GetCount()-1)
            self.SetCloudSelection(newSection)
        else:
            DEBUG('TODO: select the first one')

        dlg.Destroy()


    def OnDeleteCloud(self, e):
        oldCloud = self.cloudCombo.GetValue()
        if oldCloud != 'New...':
            action = ACTION_DELETE_CLOUD
            dlg = WarningDlg(action, False, oldCloud, self, wx.ID_ANY, 'Cloud Filer - Warning')
            dlg.ShowModal()
            status = dlg.GetEndId()
            dlg.Destroy()
            if status == wx.ID_OK:
                AppConfig.DelSection(oldCloud)
                AppConfig.WriteSettings()
                self.cloudCombo.Delete(self.cloudCombo.FindString(oldCloud))
                for section in AppConfig.GetSections():
                    if section != 'History':
                        self.SetCloudSelection(section)

    def OnNewCloud(self, e):
        self.PopupNewCloudDlg()

    def OnChangeCloud(self, e):
        newCloud = self.cloudCombo.GetValue()
        if newCloud == 'New...':
            self.PopupNewCloudDlg()
        else:
            self.SetCloudSelection(newCloud)


    def GetAccessKey(self):
        return self.accessKey.GetValue()

    def GetSecretKey(self):
        return self.secretKey.GetValue()

    def GetEncrypt(self):
        return self.encrypt.IsChecked()

    def GetPassword(self):
        return self.password.GetValue()

    def StoreKeys(self):
        return self.storeKeys.GetValue()

    def StorePassword(self):
        return self.storePassword.GetValue()


    def GetCloudService(self):
        return self.cloudCombo.GetValue()


    def GetCloudHost(self):
        return self.hostText.GetLabelText()


    def GetRootText(self):
        return self.rootText.GetLabelText()


class FileListCtrl(wx.ListCtrl):

    def __init__(self, *args, **kwargs):
        super(FileListCtrl, self).__init__(*args, **kwargs) 
            
        self._connected = False
        self._currentRemotePath = ''
        self._currentRemoteCryptoPath = ''
        self._cryptoDirMap = {}

        self.InitUI()
     

    def InitUI(self):

        self.InsertColumn(0, 'Name', width = 270)
        self.InsertColumn(1, 'Size', width = 100)
        self.InsertColumn(2, 'Type', width = 100)
        self.InsertColumn(3, 'Modified', width = 120)

        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.DoubleClick)

        self.il = wx.ImageList(16, 16)
        self.il.Add(wx.Bitmap(GetFilePath(ICON_UP_DIR)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_FOLDER)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_PLAIN)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_LOCK)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_TEXT)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_PDF)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_IMAGE)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_PACKAGE)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_MUSIC)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_FILM)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SUBTITLES)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_CDROM)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SCRIPT_PYTHON)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_WORD)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_EXCEL)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_POWERPOINT)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DOC_HTML)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SIGNATURE)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_REMOTE_FOLDER)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_CSS)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SCRIPT)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_EXE)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_LIB)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_DLL)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SMALL_ARROW_DOWN)))
        self.il.Add(wx.Bitmap(GetFilePath(ICON_SMALL_ARROW_UP)))

        self.SetImageList(self.il, wx.IMAGE_LIST_SMALL)

        self._sortAscending = True
        self.sortColumn = SORT_BY_FILENAME
        self.SwitchOnSortIndicator()

        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
        self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick)


    def Init(self, remoteIcon, comboBox, outputList, rdl, ldl):
        self._remoteIcon = remoteIcon
        self._combo = comboBox
        self._outputList = outputList
        self._remoteDirList = rdl
        self._localDirList = ldl
        self._connected = False
        self.buckets = None
        self._encryptionKey = ''
        self._salt = ''


    def OnViewRefresh(self, e):
        # If this is the remote list
        if self._remoteDirList == None:
            self.RefreshRemoteDir()
        else:
            self.RefreshDir()


    def SwitchOffSortIndicator(self):
        self.ClearColumnImage(self.sortColumn)


    def SwitchOnSortIndicator(self):
        if self._sortAscending:
            self.SetColumnImage(self.sortColumn, ICON_SMALL_ARROW_DOWN)
        else:
            self.SetColumnImage(self.sortColumn, ICON_SMALL_ARROW_UP)


    def ResetColumnSorting(self):
        self.SwitchOffSortIndicator()
        self._sortAscending = True
        self.sortColumn = SORT_BY_FILENAME
        self.SwitchOnSortIndicator()


    def OnColumnClick(self, e):
        DEBUG('OnColumnClick: ' + str(e.GetColumn()))

        self.DeleteAllItems()
        # if user clicked on the current column
        if e.GetColumn() == self.sortColumn:
            # Toggle sort order and change title
            if self._sortAscending:
                self._sortAscending = False
            else:
                self._sortAscending = True
            self.SwitchOnSortIndicator()

        else:
            self.SwitchOffSortIndicator()
            # Switch back to normal ascending sort
            self._sortAscending = True
            # Set new sort column
            self.sortColumn = e.GetColumn()
            self.SwitchOnSortIndicator()

        self.DisplayFileList()



    def OnRightDown(self, e):
        DEBUG('OnRightDown')
        try:
            # If have selected a single folder or bucklet then add Open action
            fileName = ''
            self.menu = wx.Menu()
            mi = wx.MenuItem(self.menu, wx.NewId(), 'Refresh')
            self.menu.AppendItem(mi)
            self.Bind(wx.EVT_MENU, self.OnViewRefresh, mi)
            if self.GetSelectedItemCount() == 1:
                fileName = self.GetItem(self.GetFirstSelected(), 0).GetText()
                fileType = self.GetItem(self.GetFirstSelected(), 2).GetText()
                DEBUG('fileName=' + fileName)
                DEBUG('fileType=' + fileType)
                if (fileType == 'Folder' or fileType == 'Bucket'):
                    mi = wx.MenuItem(self.menu, wx.NewId(), 'Open')
                    self.menu.AppendItem(mi)
                    self.Bind(wx.EVT_MENU, self.DoubleClick, mi)

            # If this is the remote list
            if self._remoteDirList == None:
            
                # If we are connected
                if self._connected:
                    pathBits = self._currentRemotePath.split('/')

                    if self.menu.GetMenuItemCount() > 0:
                        self.menu.AppendSeparator()

                    # If at root showing bucket list
                    if len(pathBits) < 2 or pathBits[1] == '':
                        mi = wx.MenuItem(self.menu, wx.NewId(), 'New Bucket')
                        self.menu.AppendItem(mi)
                        self.Bind(wx.EVT_MENU, self.OnNewBucket, mi)
                        if self.GetSelectedItemCount() > 0:
                            mi = wx.MenuItem(self.menu, wx.NewId(), 'Delete')
                            self.menu.AppendItem(mi)
                            self.Bind(wx.EVT_MENU, self.OnDeleteBucket, mi)
                    else:

                        if self.GetSelectedItemCount() == 1:
                            fileType = self.GetItem(self.GetFirstSelected(), 2).GetText()
                            DEBUG('fileType=' + fileType)

                        mi = wx.MenuItem(self.menu, wx.NewId(), 'New Folder')
                        self.menu.AppendItem(mi)
                        self.Bind(wx.EVT_MENU, self.OnNewFolder, mi)
                        if self.GetSelectedItemCount() > 0:
                            mi = wx.MenuItem(self.menu, wx.NewId(), 'Delete')
                            self.menu.AppendItem(mi)
                            self.Bind(wx.EVT_MENU, self.OnDelete, mi)
                            self.menu.AppendSeparator()
                            mi = wx.MenuItem(self.menu, wx.NewId(), 'Download')
                            self.menu.AppendItem(mi)
                            self.Bind(wx.EVT_MENU, self.OnDownload, mi)

            # Else this is the local list
            else:
                # If something is selected
                if self.GetSelectedItemCount() > 0:
                    # If the remote side can accept stuff and the up dir (always first) is not selected
                    if self._remoteDirList.CanUpload() and self.GetItemText(self.GetFirstSelected()) != '..':
                        if self.menu.GetMenuItemCount() > 0:
                            self.menu.AppendSeparator()
                        mi = wx.MenuItem(self.menu, wx.NewId(), 'Upload')
                        self.menu.AppendItem(mi)
                        self.Bind(wx.EVT_MENU, self.OnUpload, mi)

            if self.menu.GetMenuItemCount() > 0:
                self.PopupMenu(self.menu, e.GetPosition())

        except Exception as ex:
            errorMessage = str(ex)
            self._outputList.Append('Error: ' + errorMessage)


    def CanUpload(self):
        if self._connected:
            # If not at bucket list
            pathBits = self._currentRemotePath.split('/')
            if len(pathBits) > 1 and pathBits[1] != '':
                return True

        return False


    def OnUpload(self, e):
        dlg = CloudActionDlg(ACTION_UPLOAD_FILE, self, wx.ID_ANY, 'Cloud Filer - Upload')
        thread = UploadThread(dlg, self)
        thread.start()
        dlg.ShowModal()
        thread.join()
    
        self._outputList.Append(thread.GetMessages())

        self._remoteDirList.RefreshRemoteDir()
        dlg.Destroy()
            

    def OnDownload(self, e):
        dlg = CloudActionDlg(ACTION_DOWNLOAD_FILE, self, wx.ID_ANY, 'Cloud Filer - Download')
        thread = DownloadThread(dlg, self)
        thread.start()
        dlg.ShowModal()
        thread.join()
    
        self._outputList.Append(thread.GetMessages())

        self._localDirList.RefreshDir()
        dlg.Destroy()



    def OnNewBucket(self, e):
        dlg = NewBucketOrFolderDlg(True, self, wx.ID_ANY, 'Create new bucket')
        if dlg.ShowModal() == wx.ID_OK:
            name = dlg.GetName()
            try:
                self._connection.create_bucket(name)
                self.buckets = self._connection.get_all_buckets()
                self.ChangeRemoteDir(self._cloudRootText)

            except S3CreateError as ex:
                self._outputList.Append('S3 Create Error: bucket name must be globally unique!')

            except S3ResponseError as ex:
                if len(ex.error_message) > 0:
                    errorMessage = ex.error_message
                else:
                    errorMessage = str(ex)
                self._outputList.Append('S3 Error: "' + errorMessage + '"')

            except Exception as ex:
                errorMessage = str(ex)
                self._outputList.Append('Error: ' + errorMessage)

        dlg.Destroy()


    def CreateNewFile(self, fileName, contents):
        try:
            bucket, keyName = self.GetCurrentRemoteLocation()
            if not keyName.startswith('/'):
                keyName = '/' + keyName
            keyName = keyName + '/'
            k = Key(bucket)
            k.key = keyName + fileName.replace('\\', '/')
            DEBUG('key=' + k.key)
            DEBUG('contents=' + contents)
            k.set_contents_from_string(contents)

        except S3ResponseError as ex:
            if len(ex.error_message) > 0:
                errorMessage = ex.error_message
            else:
                errorMessage = str(ex)
            self._outputList.Append('S3 Error: "' + errorMessage + '"')

        except Exception as ex:
            errorMessage = str(ex)
            self._outputList.Append('Error: ' + errorMessage)


    def CreateNewFolder(self, newFolderName):
        DEBUG('CreateNewFolder: newFolderName=' + newFolderName)
        try:
            bucket, keyName = self.GetCurrentRemoteLocation()
            if not keyName.startswith('/'):
                keyName = '/' + keyName
            keyName = keyName + newFolderName + '/'
            k = Key(bucket)
            fileName = '.$folder$'
            k.key = keyName + fileName.replace('\\', '/')
            DEBUG('key=' + k.key)
            k.set_contents_from_string('dummy file')

        except S3ResponseError as ex:
            if len(ex.error_message) > 0:
                errorMessage = ex.error_message
            else:
                errorMessage = str(ex)
            self._outputList.Append('S3 Error: "' + errorMessage + '"')

        except Exception as ex:
            errorMessage = str(ex)
            self._outputList.Append('Error: ' + errorMessage)


    def GetNewCryptoFolderName(self):
        if len(self.cryptoDirHoles) > 0:
            newNum = self.cryptoDirHoles[0]
            self.cryptoDirHoles = self.cryptoDirHoles[1:]
        else:
            newNum = self.cryptoDirNextFree
            self.cryptoDirNextFree = self.cryptoDirNextFree + 1

        newName = "_CFD%07d" % (newNum)
        DEBUG('GetNewCryptoFolderName: newName=' + newName)
        return newName

           
    def RemoveExistingCryptoFileName(self, cleartextFileName):
        DEBUG('RemoveExistingCryptoFileName: cleartextFileName=' + cleartextFileName)
        self.cryptoFiles = filter(lambda (s1, s2, s3, s4): s2 != cleartextFileName, self.cryptoFiles)


    def GetAndRemoveExistingCryptoFileName(self, cleartextFileName):
        DEBUG('GetAndRemoveExistingCryptoFileName: cleartextFileName=' + cleartextFileName)
        fileNameList = filter(lambda (s1, s2, s3, s4): s2 == cleartextFileName, self.cryptoFiles)
        self.cryptoFiles = filter(lambda (s1, s2, s3, s4): s2 != cleartextFileName, self.cryptoFiles)
        DEBUG('GetAndRemoveExistingCryptoFileName: cryptoFiles=' + str(self.cryptoFiles))
        if len(fileNameList) == 0:
            DEBUG('GetAndRemoveExistingCryptoFileName: not found')
            return None
        else:
            (s1, s2, s3, s4) = fileNameList[0]
            DEBUG('GetAndRemoveExistingCryptoFileName: found ' + s1)
            return s1


    def GetAndRemoveExistingCryptoDirName(self, cleartextDirName):
        DEBUG('GetAndRemoveExistingCryptoDirName: cleartextDirName=' + cleartextDirName)
        dirNameList = filter(lambda (s1, s2, s3, s4): s2 == cleartextDirName, self.cryptoDirs)
        self.cryptoDirs = filter(lambda (s1, s2, s3, s4): s2 != cleartextDirName, self.cryptoDirs)
        DEBUG('GetAndRemoveExistingCryptoDirName: cryptoDirs=' + str(self.cryptoDirs))
        if len(dirNameList) == 0:
            DEBUG('GetAndRemoveExistingCryptoDirName: not found')
            return None
        else:
            (s1, s2, s3, s4) = dirNameList[0]
            DEBUG('GetAndRemoveExistingCryptoDirName: found ' + s1)
            return s1


    def CryptoDirNameExists(self, cleartextDirName):
        DEBUG('CryptoDirNameExists: cleartextDirName=' + cleartextDirName)
        dirNameList = filter(lambda (s1, s2, s3, s4): s2 == cleartextDirName, self.cryptoDirs)
        if len(dirNameList) > 0:
            DEBUG('CryptoDirNameExists: True')
            return True
        else:
            DEBUG('CryptoDirNameExists: False')
            return False


    def GetCryptoFolder(self, cleartextDirName):
        DEBUG('GetCryptoFolder: cleartextDirName=' + cleartextDirName)
        dirNameList = filter(lambda (s1, s2, s3, s4): s2 == cleartextDirName, self.cryptoDirs)
        if len(dirNameList) == 1:
            (s1, s2, s3, s4) = dirNameList[0]
            DEBUG('GetCryptoFolder: found ' + s1)
            return s1
        else:
            raise Exception('GetCryptoFolder ' + cleartextDirName + ' found ' + str(len(dirNameList)) + ' entries')


    def GetCryptoFileName(self, cleartextFileName):
        DEBUG('GetCryptoFileName: cleartextFileName=' + cleartextFileName)
        fileNameList = filter(lambda (s1, s2, s3, s4): s2 == cleartextFileName, self.cryptoFiles)
        if len(fileNameList) == 1:
            (s1, s2, s3, s4) = fileNameList[0]
            DEBUG('GetCryptoFileName: found ' + s1)
            return s1
        else:
            raise Exception('GetCryptoFileName ' + cleartextFileName + ' found ' + str(len(fileNameList)) + ' entries')


    def GetCleartextFileName(self, cryptoFileName):
        DEBUG('GetCleartextFileName: cryptoFileName=' + cryptoFileName)
        fileNameList = filter(lambda (s1, s2, s3, s4): s1 == cryptoFileName, self.cryptoFiles)
        if len(fileNameList) == 1:
            (s1, s2, s3, s4) = fileNameList[0]
            DEBUG('GetCleartextFileName: found ' + s2)
            return s2
        else:
            raise Exception('GetCleartextFileName ' + cryptoFileName + ' found ' + str(len(fileNameList)) + ' entries')


    def GetCleartextDirName(self, cryptoDirName):
        cryptoDirName = cryptoDirName.rstrip('/')
        DEBUG('GetCleartextDirName: cryptoDirName=' + cryptoDirName)
        dirNameList = filter(lambda (s1, s2, s3, s4): s1 == cryptoDirName, self.cryptoDirs)
        if len(dirNameList) == 1:
            (s1, s2, s3, s4) = dirNameList[0]
            DEBUG('GetCleartextDirName: found ' + s2)
            return s2
        else:
            raise Exception('GetCleartextDirName ' + cryptoDirName + ' found ' + str(len(dirNameList)) + ' entries')


    def GetNewCryptoFileName(self):
        if len(self.cryptoFileHoles) > 0:
            newNum = self.cryptoFileHoles[0]
            self.cryptoFileHoles = self.cryptoFileHoles[1:]
        else:
            newNum = self.cryptoFileNextFree
            self.cryptoFileNextFree = self.cryptoFileNextFree + 1

        newName = "_CFL%07d" % (newNum)
        DEBUG('GetNewCryptoFileName: newName=' + newName)
        return newName

           
    def GetEncryptionKey(self):
        DEBUG('Use pbkdf2_sha256 to generate the key from the password')
        DEBUG('password=' + self._password)
        if (len(self._salt) > 0):
            DEBUG('len(salt)=' + str(len(self._salt)))
            hashAndSalt = pbkdf2_sha256.encrypt(self._password, salt=self._salt, rounds=50000)
        else:
            DEBUG('No salt passed in')
            hashAndSalt = pbkdf2_sha256.encrypt(self._password, salt_size=32, rounds=50000)
        DEBUG('hashAndSalt=' + hashAndSalt)
        # split out the salt and EK
        cryptoStuff = hashAndSalt.split('$')
        salt = cryptoStuff[3]
        while len(salt) % 4 != 0:
            salt = salt + '='
        DEBUG('b64salt=' + salt)
        self._salt = b64decode(salt, CTA_ALTCHARS)
        DEBUG('len(decoded salt)=' + str(len(self._salt)))
        encryptionKey = cryptoStuff[4]
        while len(encryptionKey) % 4 != 0:
            encryptionKey = encryptionKey + '='
        DEBUG('encryptionKey=' + encryptionKey)
        self._encryptionKey = b64decode(encryptionKey, CTA_ALTCHARS)
        DEBUG('len(decoded encryptionKey)=' + str(len(self._encryptionKey)))


    def UploadNewContentsInfo(self):
        DEBUG('UploadNewContentsInfo: len(self._encryptionKey)=' + str(len(self._encryptionKey)))
        if len(self._encryptionKey) == 0 or len(self._salt) == 0:
            self.GetEncryptionKey()

        # Create a file called _CFCI containing list of real and crypto dir names
        dirContentsClearText = FOLDER_CONTENTS_HEADER + '\n' + self._currentRemoteCryptoPath + '\n'
        for cryptoDir in self.cryptoDirs:
            (cryptoDirName, dirName, dirSize, dirTime) = cryptoDir
            DEBUG('cryptoDirName=' + cryptoDirName)
            DEBUG('dirName=' + dirName)
            DEBUG('dirSize=' + dirSize)
            DEBUG('dirTime=' + dirTime)
            dirContentsClearText = dirContentsClearText + str(cryptoDir) + '\n'

        for cryptoFile in self.cryptoFiles:
            (cryptoFileName, fileName, fileSize, fileTime) = cryptoFile
            DEBUG('cryptoFileName=' + cryptoFileName)
            DEBUG('fileName=' + fileName)
            DEBUG('fileSize=' + fileSize)
            DEBUG('fileTime=' + fileTime)
            dirContentsClearText = dirContentsClearText + str(cryptoFile) + '\n'

        DEBUG('dirContentsClearText=' + dirContentsClearText)
        dirContentsCypherStr = EncryptStr(dirContentsClearText, self._encryptionKey)
        DEBUG('dirContentsCypherStr=' + dirContentsCypherStr)

        dirContentsStr = b64encode(self._salt) + '\n' + dirContentsCypherStr
        DEBUG('dirContentsStr=' + dirContentsStr)
        self.CreateNewFile('_CFCI', dirContentsStr)


    def AddedCryptoFile(self, cryptoFileName, newFileName, fileSize, lastModTime):
        DEBUG('AddedCryptoFile: cryptoFileName=' + cryptoFileName)
        DEBUG('AddedCryptoFile: newFileName=' + newFileName)
        DEBUG('AddedCryptoFile: fileSize=' + fileSize)
        DEBUG('AddedCryptoFile: lastModTime=' + lastModTime)
        self.cryptoFiles.append((cryptoFileName, newFileName, fileSize, lastModTime))


    def AddedCryptoDir(self, cryptoFolderName, newFolderName):
        DEBUG('AddedCryptoDir: cryptoFolderName=' + cryptoFolderName)
        DEBUG('AddedCryptoDir: newFolderName=' + newFolderName)
        self.cryptoDirs.append((cryptoFolderName, newFolderName, '0', '0'))


    def OnNewFolder(self, e):
        dlg = NewBucketOrFolderDlg(False, self, wx.ID_ANY, 'Create new folder')
        if dlg.ShowModal() == wx.ID_OK:
            newFolderName = dlg.GetName()
            if len(self._password) > 0:
                cryptoFolderName = self.GetNewCryptoFolderName()
                self.CreateNewFolder(cryptoFolderName)
                self.AddedCryptoDir(cryptoFolderName, newFolderName)
                self.UploadNewContentsInfo()
            else:
                self.CreateNewFolder(newFolderName)

        dlg.Destroy()
        self.RefreshRemoteDir()


    def OnRename(self, e):
        if self.GetSelectedItemCount() == 1:
            oldName = self.GetItemText(self.GetFirstSelected())
            if oldName != None and oldName != '':
                dlg = RenameDlg(oldName, self, wx.ID_ANY, 'Rename')
                if dlg.ShowModal() == wx.ID_OK:
                    newName = dlg.GetNewName()
                    # TODO doit

                dlg.Destroy()
        

    def OnDeleteBucket(self, e):
        prompt = True
        cancelled = False
        currentItem = self.GetFirstSelected()
        while currentItem != -1 and not cancelled:
            bucketName = self.GetItem(currentItem, 0).GetText()
            fileType = self.GetItem(currentItem, 2).GetText()
            if fileType != 'Bucket':
                self._outputList.Append('Expecetd a bucket: ' + fileType)
                currentItem = self.GetNextSelected(currentItem)
                continue

            objectName = bucketName
            action = ACTION_DELETE_BUCKET
            dlg = WarningDlg(action, False, objectName, self, wx.ID_ANY, 'Cloud Filer - Warning')
            dlg.ShowModal()
            status = dlg.GetEndId()
            dlg.Destroy()
            if status == wx.ID_OK:

                try:
                    # Delete bucket contents
                    bucket = self.GetBucket(bucketName)
                    keys = bucket.list()
                    # create deletethread(keys)
                    dlg = CloudActionDlg(ACTION_DELETE_BUCKET, self, wx.ID_ANY, 'Cloud Filer - Delete Bucket')
                    dlg.SetActionMsg(bucketName)
                    thread = DeleteThread(dlg, keys)
                    thread.start()
                    dlg.ShowModal()
                    thread.join()
                    self._outputList.Append(thread.GetMessages())

                    cancelled = dlg.GetCancelled()

                    # If the user didn't cancel and the bucket is empty
                    bucketEmpty = True
                    keys = bucket.list()
                    for key in keys:
                        bucketEmpty = False
                        break

                    if not cancelled:
                        if bucketEmpty:
                            # Delete bucket
                            self._connection.delete_bucket(bucketName)
                            self._outputList.Append('Deleted bucket ' + bucketName)
                        else:
                            self._outputList.Append('Bucket deletion failed')
                    else:
                        self._outputList.Append('Bucket deletion cancelled')

                except S3ResponseError as ex:
                    errorMessage = ex.error_message
                    self._outputList.Append('S3 Error: "' + errorMessage + '"')

                except Exception as ex:
                    errorMessage = str(ex)
                    self._outputList.Append('Error: ' + errorMessage)

    #            dlg.Destroy()

            elif status == wx.ID_CANCEL:
                cancelled = True
                self._outputList.Append('Deletion cancelled')
            else:
                self._outputList.Append('Skipping deletion of ' + bucketName)

            currentItem = self.GetNextSelected(currentItem)

        self.buckets = self._connection.get_all_buckets()
        self.ChangeRemoteDir(self._cloudRootText)


    def RecursiveList(self, bucket, keyName):
        keys = bucket.list(keyName, '/')
        for key in keys:
            DEBUG('key.name=' + key.name)
            if key.name.endswith('/'):
                keys2 = self.RecursiveList(bucket, key.name)
                keys = chain(keys, keys2)

        return keys


    def OnDelete(self, e):
        prompt = True
        cancelled = False
        currentItem = self.GetFirstSelected()
        while currentItem != -1 and not cancelled:
            fileName = self.GetItem(currentItem, 0).GetText()
            fileType = self.GetItem(currentItem, 2).GetText()
            showApplyToAll = False

            if fileType == 'Folder':
                action = ACTION_DELETE_FOLDER
            else:
                action = ACTION_DELETE_FILE
            if prompt:
                if self.GetSelectedItemCount() > 1:
                    showApplyToAll = True
                dlg = WarningDlg(action, showApplyToAll, fileName, self, wx.ID_ANY, 'Cloud Filer - Warning')
                dlg.ShowModal()
                status = dlg.GetEndId()
                dlg.Destroy()
            if status == wx.ID_OK:
                if showApplyToAll and dlg.GetApplyAll():
                    prompt = False
                try:
                    bucket, keyName = self.GetCurrentRemoteLocation()

                    if action == ACTION_DELETE_FILE:
                        # Just delete the file in main thread
                        if len(self._password) > 0:
                            # Remove file from crypto contents info file
                            savedCleartextFileName = fileName
                            fileName = self.GetAndRemoveExistingCryptoFileName(fileName)
                            self.UploadNewContentsInfo()
                        key = Key(bucket)
                        key.key = keyName + fileName.replace('\\', '/')
                        DEBUG('key=' + key.key)
                        key.delete()
                        if len(self._password) > 0:
                            fileName = savedCleartextFileName
                        else:
                            fileName = key.name
                        self._outputList.Append('Deleted file ' + fileName)
                    else:
                        # Delete folder
                        if len(self._password) > 0:
                            savedCleartextDirName = fileName
                            fileName = self.GetAndRemoveExistingCryptoDirName(fileName)
                            self.UploadNewContentsInfo()
                        keyName = keyName.lstrip('/') + fileName + '/'
                        DEBUG('List folder for deletion: prefix = ' + keyName)
                        keys = self.RecursiveList(bucket, keyName)

                        dlg = CloudActionDlg(ACTION_DELETE_BUCKET, self, wx.ID_ANY, 'Cloud Filer - Delete Folder')
                        dlg.SetActionMsg(keyName)
                        thread = DeleteThread(dlg, keys)
                        thread.start()
                        dlg.ShowModal()
                        thread.join()
                        self._outputList.Append(thread.GetMessages())

                        cancelled = dlg.GetCancelled()

                        # If the user didn't cancel
                        if not cancelled:
                            if len(self._password) > 0:
                                dirName = savedCleartextDirName + ' (' + keyName + ')'
                            else:
                                dirName = keyName
                            self._outputList.Append('Deleted folder ' + dirName)
                        else:
                            self._outputList.Append('Folder deletion cancelled')

                except S3ResponseError as ex:
                    errorMessage = ex.error_message
                    self._outputList.Append('S3 Error: "' + errorMessage + '"')

                except Exception as ex:
                    errorMessage = str(ex)
                    self._outputList.Append('Error: ' + errorMessage)

    #            dlg.Destroy()

            elif status == wx.ID_CANCEL:
                cancelled = True
                self._outputList.Append('Deletion cancelled')
            else:
                self._outputList.Append('Skipping deletion of ' + fileName)

            currentItem = self.GetNextSelected(currentItem)

        self.RefreshRemoteDir()
        

    def DoubleClick(self, event):
#        newDir = event.GetText()
#        fileType = self.GetItem(event.GetIndex(), 2).GetText()
        
        if self.GetSelectedItemCount() == 1:
            fileName = self.GetItem(self.GetFirstSelected(), 0).GetText()
            fileType = self.GetItem(self.GetFirstSelected(), 2).GetText()
            DEBUG('fileName=' + fileName)
            DEBUG('fileType=' + fileType)
            if fileType == 'Folder' or fileType == 'Bucket':
                DEBUG('it is a folder')
                if self._connected:
                    self.ChangeRemoteDir(fileName)

                else:
                    # Try to do local cd
                    if (self.SetDir(fileName)):
                        cwd = os.getcwd()
                        if self._combo.FindString(cwd) == wx.NOT_FOUND:
                            self._combo.Append(cwd)

                        self._combo.SetStringSelection(cwd)
            else:
                DEBUG('not a folder')


    def WindowsRootDir(self, pathStr):
        if len(pathStr) != 3:
            return False

        return re.match(r'[A-Z]:\\', pathStr)



    def RefreshDir(self):
        self.SetDir(os.getcwd())



    def SetDir(self, dirPath):

        try:
            os.chdir(dirPath)

        except OSError:
            return False

        self.DeleteAllItems()

        self._outputList.Append('lcd ' + dirPath)

        self.displayRoot = os.getcwd() != '/' and not self.WindowsRootDir(os.getcwd())

        files = os.listdir(u'.')

        self.currentFolderFolders = []
        self.currentFolderFiles = []
        # Add folders
        for i in files:
            if os.path.isdir(i):
                DEBUG('isDir: ' + i)
#TODO                if fnmatch.fnmatch(i, gbl_ignore_pattern):
#                    continue

                # Skip any folder we can't access
                try:
                    os.listdir(ensure_unicode(i))

                except Exception as ex:
#                    errorMessage = str(ex)
#                    self._outputList.Append('Error: ' + errorMessage)
                    continue  

                size = os.path.getsize(i)
                sec = os.path.getmtime(i)
                timeStr = time.strftime('%Y-%m-%d %H:%M', time.localtime(sec))

                self.currentFolderFolders.append((i, '', 'Folder', timeStr, 1))

        # Add files
        for i in files:
            if not os.path.isdir(i):
#                (name, ext) = os.path.splitext(i)
#TODO                if fnmatch.fnmatch(name, gbl_ignore_pattern):
#                    continue

#                ex = ext[1:].lower()
                
#                fileType, imageIndex = self.GetFileType(ex)

                try:
                    size = os.path.getsize(i)
                    sec = os.path.getmtime(i)
#                    if len(ext) > 0:
#                        name = name + ext

                    timeStr = time.strftime('%Y-%m-%d %H:%M', time.localtime(sec))

                    self.addFileToCurrentFolderList(i, str(size), timeStr)

                except Exception as ex:
                    continue  

        self.ResetColumnSorting()
        self.DisplayFileList()
        return True


    def GetFileType(self, ex):

        if (ex == 'gpg' or ex == 'pgp'):
            fileType = 'Encrypted'
            imageIndex = 3
        elif (ex == 'tc'):
            fileType = 'TrueCrypt'
            imageIndex = 3
        elif (ex == 'txt' or ex == 'log'):
            fileType = 'Text'
            imageIndex = 4
        elif (ex == 'nfo'):
            fileType = 'Info'
            imageIndex = 4
        elif (ex == 'pdf'):
            fileType = 'Document'
            imageIndex = 5
        elif (ex == 'png' or ex == 'jpg' or ex == 'bmp' or ex == 'gif'):
            fileType = 'Image'
            imageIndex = 6
        elif (ex == 'tgz' or ex == 'gz' or ex == 'zip' or ex == 'tar' or ex == 'rar' or ex == 'msi' or ex == 'msp'):
            fileType = 'Archive'
            imageIndex = 7
        elif (ex == 'wav' or ex == 'mp3' or ex == 'm4a'):
            fileType = 'Audio'
            imageIndex = 8
        elif (ex == 'avi' or ex == 'mpg' or ex == 'mp4' or ex == 'ogm' or ex =='flv'):
            fileType = 'Video'
            imageIndex = 9
        elif (ex == 'idx' or ex == 'sub' or ex == 'srt'):
            fileType = 'Subtitles'
            imageIndex = 10
        elif (ex == 'iso'):
            fileType = 'Disk Image'
            imageIndex = 11
        elif (ex == 'py'):
            fileType = 'Text'
            imageIndex = 12
        elif (ex == 'odt' or ex == 'doc' or ex == 'docx' or ex == 'docm'):
            fileType = 'Document'
            imageIndex = 13
        elif (ex == 'ods' or ex == 'xls' or ex == 'xlsx' or ex == 'xlsm'):
            fileType = 'Spreadsheet'
            imageIndex = 14
        elif (ex == 'odp' or ex == 'ppt' or ex == 'pptx' or ex == 'pptm' or ex == 'ppsx' or ex == 'ppsm'):
            fileType = 'Presentation'
            imageIndex = 15
        elif (ex == 'odg' or ex == 'dia' or ex == 'vsd' or ex == 'vdx' or ex == 'vsx' or ex == 'vss'):
            fileType = 'Diagram'
            imageIndex = 6
        elif (ex == 'htm' or ex == 'html'):
            fileType = 'Web Page'
            imageIndex = 16
        elif (ex == 'sig' or ex == 'asc'):
            fileType = 'Signature'
            imageIndex = 17
        elif (ex == 'css'):
            fileType = 'Style Sheet'
            imageIndex = ICON_CSS
        elif (ex == 'js' or ex == 'sh' or ex == 'vbs' or ex == 'pl' or ex == 'ps2' or ex == 'ps1' or ex == 'bat' or ex == 'jse' or ex == 'reg' or ex == 'vb'):
            fileType = 'Script'
            imageIndex = ICON_SCRIPT
        elif (ex == 'exe'):
            fileType = 'Executable'
            imageIndex = ICON_EXE
        elif (ex == 'dll' or ex == 'so'):
            fileType = 'Dynamic Library'
            imageIndex = ICON_DLL
        elif (ex == 'lib' or ex == 'a'):
            fileType = 'Static Library'
            imageIndex = ICON_LIB
        else:
            fileType = ''
            imageIndex = 2

        return (fileType, imageIndex)


    def S3Connect(self, accessKey, secretKey, password, cloudService, cloudHost, cloudRootText):
        try:
            self._connected = False
            self._combo.Clear()
            self._combo.SetValue('')
            self._cryptoDirMap.clear()
            if not self.buckets is None:
                self.buckets = None
            self.DeleteAllItems() # I am a ListCtrl and should be emptied
            self._outputList.Append('Connecting to ' + cloudService + '...')
            self._connection = S3Connection(accessKey, secretKey, host=cloudHost)
            self.buckets = self._connection.get_all_buckets()
            self._outputList.Append('Connected and found ' + str(len(self.buckets)) + ' buckets')
            self._cloudRootText = cloudRootText
            self._currentRemotePath = self._cloudRootText
            self._currentRemoteCryptoPath = self._cloudRootText

            self._password = password
            self._salt = ''
            if len(password) > 0:
                self.SetBackgroundColour(wx.Colour(200,240,200))
                self._remoteIcon.SetBitmap(wx.Bitmap(GetFilePath(ICON_GREEN_SHIELD)))
            else:
                self.SetBackgroundColour(wx.Colour(255,255,255))
                self._remoteIcon.SetBitmap(wx.Bitmap(GetFilePath(ICON_RED_SHIELD)))

            self._connected = True

        except S3ResponseError as ex:
            errorMessage = ex.error_message
            if errorMessage is not None:
                self._outputList.Append('S3 Error: "' + errorMessage + '"')
            else:
                self._outputList.Append('S3 Error')

        except Exception as ex:
            errorMessage = str(ex)
            self._outputList.Append('Error: ' + errorMessage)


    def DisplayBucketList(self):
        DEBUG('DisplayBucketList')
        self.currentFolderFolders = []
        self.currentFolderFiles = []
        for i in self.buckets:
            self.currentFolderFolders.append((i.name, '', 'Bucket', '', 18))

        self.displayRoot = False
        self.ResetColumnSorting()
        self.DisplayFileList()


    def RefreshRemoteDir(self):
        self.ChangeRemoteDir(self._currentRemotePath)


    def ChangeRemoteDir(self, newDir):
        DEBUG('ChangeRemoteDir: newDir=' + newDir)
        if not self._connected:
            DEBUG('Not connected: ignore refresh request')
            return

        newDir = newDir.rstrip('/')
        self._outputList.Append('cd ' + newDir)
        # If it's an absolute path
        if newDir.startswith(self._cloudRootText):

            if not self._connected:
                self._outputList.Append('Need to connect first')
                return False

            self._currentRemotePath = newDir

        else:
            # Should be a relatve path and => should have root in _currentRemotePath
            if self._currentRemotePath == None or not self._currentRemotePath.startswith(self._cloudRootText):
                self._outputList.Append('Invalid remote cloud service (expect ' + self._cloudRootText + ' at start) - ' + newDir)

            else:
                if newDir == '..':
                    currentPathBits = self._currentRemotePath.split('/')
                    self._currentRemotePath = '/'.join(currentPathBits[:len(currentPathBits)-1])

                    if len(self._password) > 0:
                        currentCryptoPathBits = self._currentRemoteCryptoPath.split('/')
                        self._currentRemoteCryptoPath = '/'.join(currentCryptoPathBits[:len(currentCryptoPathBits)-1])
                else:
                    self._currentRemotePath = self._currentRemotePath + '/' + newDir
                    if len(self._password) > 0:
                        bucket, bucketListPrefix = self.GetCurrentRemoteLocation()
                        if bucket is not None:
                            self._currentRemoteCryptoPath = self._currentRemoteCryptoPath + '/' + self.GetCryptoFolder(newDir)
                        else:
                            self._currentRemoteCryptoPath = self._currentRemoteCryptoPath + '/' + newDir
        
        self.DisplayRemoteFolder()

        cwd = self._currentRemotePath
        if self._combo.FindString(cwd) == wx.NOT_FOUND:
            self._combo.Append(cwd)
            self._cryptoDirMap[cwd] = self._currentRemoteCryptoPath

        self._combo.SetStringSelection(cwd)

        return True


    def SetRemoteCryptoDirFromCache(self, newDir):
        DEBUG('SetRemoteCryptoDirFromCache: newDir=' + newDir)
        cryptoDir = self._cryptoDirMap[newDir]
        if cryptoDir is None:
            raise Exception('Crypto remote folder not found for ' + newDir)
        self._currentRemoteCryptoPath = cryptoDir


    def GetConnection(self):
        return self._connection


    def GetBucket(self, bucketName):
        for bucket in self.buckets:
            if bucket.name == bucketName:
                return bucket

        raise Exception('Bucket not found: ' + bucketName)


    def GetCurrentRemoteLocation(self):
        if len(self._password) > 0:
            DEBUG('GetCurrentRemoteLocation: currentRemoteCryptoPath=' + self._currentRemoteCryptoPath)
            pathBits = self._currentRemoteCryptoPath.split('/')
        else:
            DEBUG('GetCurrentRemoteLocation: currentRemotePath=' + self._currentRemotePath)
            pathBits = self._currentRemotePath.split('/')
        # If at root: list of buckets
        if len(pathBits) < 2 or pathBits[1] == '':
#            raise Exception('Not in a bucket')
            return None, '/'
        else:
            DEBUG(str(pathBits))
            bucketName = pathBits[1]
            if len(pathBits) > 2:
                bucketListPrefix = '/'.join(pathBits[2:]) + '/'
            else:
                bucketListPrefix = '/'
            return self.GetBucket(bucketName), bucketListPrefix


    def GetEncryptedDirContents(self, bucket, bucketListPrefix):
        # _CFFn or _CFDn
        # FileFreeList is a list of numbers which are holes
        # FileNextFree int
        # get _CFCI which is the contents information file

        self.cryptoDirHoles = []
        self.cryptoFileHoles = []
        self.cryptoDirNextFree = 1
        self.cryptoFileNextFree = 1
        self.cryptoDirs = []
        self.cryptoFiles = []

        self.currentFolderFolders = []
        self.currentFolderFiles = []

        try:
            fileKey = bucketListPrefix + '_CFCI'
            DEBUG('DownloadFile: fileKey = ' + fileKey)
            k = bucket.get_key(fileKey)

            # Seem to have information from S3 service
            if k is None:
                DEBUG('Encrypted contents file not found')
                self.UploadNewContentsInfo() # Create an empty encrypted contents file with a salt
            else:
                DEBUG('key=' + k.key)
                fileSize = k.size
                DEBUG('fileSize=' + str(fileSize))
                
                folderContentsSaltAndCypherStr = k.get_contents_as_string()
                DEBUG('folderContentsSaltAndCypherStr=' + folderContentsSaltAndCypherStr)

                # Split salt from cypher
                splitPoint = folderContentsSaltAndCypherStr.index('\n')
                b64salt = folderContentsSaltAndCypherStr[0:splitPoint]
                folderContentsCypherStr = folderContentsSaltAndCypherStr[splitPoint+1:]

                # Check the salt matches
                DEBUG('b64salt=' + b64salt)
                remoteSalt = b64decode(b64salt)
                if self._salt != remoteSalt:
                    self._salt = remoteSalt
                    self.GetEncryptionKey()

                try:
                    DEBUG('folderContentsCypherStr=' + folderContentsCypherStr)
                    contentsListStr = DecryptStr(folderContentsCypherStr, self._encryptionKey)
                    DEBUG('contentsListStr=' + contentsListStr)

                    # Check the contents info decrypted ok
                    contentsList = contentsListStr.split('\n')
                    if contentsList[0] != FOLDER_CONTENTS_HEADER:
                        raise Exception('Failed to decrypt')
                except Exception as ex:
                    DEBUG('Decryption ex: ' + str(ex))
                    raise Exception('Failed to decrypt')

                DEBUG('currentRemoteCryptoPath=' + self._currentRemoteCryptoPath)
                if self._currentRemoteCryptoPath != contentsList[1]:
                    errorMessage = 'Crypto path mismatch: expected ' + self._currentRemoteCryptoPath + ' found ' + contentsList[1]
                    DEBUG(errorMessage)
                    self._outputList.Append(errorMessage)

                remoteObjList = []
                for i in range(2, len(contentsList) - 1):
#                    (cryptoObjName, objName, objSize, objTime) = literal_eval(contentsList[i])
                    remoteObjList.append(literal_eval(contentsList[i]))

                # Sort the remote list by crypto name so we can work out holes we can reuse and discard duplicate entries
                remoteObjList = sorted(remoteObjList, key=lambda (s1, s2, s3, s4): s1)

                expectedNextDirNumber = 1
                expectedNextFileNumber = 1
                for remoteObj in remoteObjList:
                    (cryptoObjName, objName, objSize, objTime) = remoteObj
                    DEBUG('cryptoObjName=' + cryptoObjName)
                    DEBUG('objName=' + objName)
                    DEBUG('objSize=' + objSize)
                    DEBUG('objTime=' + objTime)
                    objNumber = int(cryptoObjName[4:])
                    if cryptoObjName.startswith('_CFD'):
                        DEBUG('Found a dir')
                        while objNumber > expectedNextDirNumber:
                            self.cryptoDirHoles.append(expectedNextDirNumber)
                            expectedNextDirNumber = expectedNextDirNumber + 1

                        if objNumber != expectedNextDirNumber:
                            errorMessage = 'Duplicate remote dir ref discarded: cryptoObjName=' + cryptoObjName + ' objName=' + objName
                            DEBUG(errorMessage)
                            self._outputList.Append(errorMessage)
                        else:
                            self.AddedCryptoDir(cryptoObjName, objName)
                            expectedNextDirNumber = objNumber + 1
                            self.currentFolderFolders.append((objName, '', 'Folder', '', 18))

                    elif cryptoObjName.startswith('_CFL'):
                        DEBUG('Found a file')
                        while objNumber > expectedNextFileNumber:
                            DEBUG('Found a hole: ' + str(expectedNextFileNumber))
                            self.cryptoFileHoles.append(expectedNextFileNumber)
                            expectedNextFileNumber = expectedNextFileNumber + 1

                        if objNumber != expectedNextFileNumber:
                            errorMessage = 'Duplicate remote file ref discarded: cryptoObjName=' + cryptoObjName + ' objName=' + objName
                            DEBUG(errorMessage)
                            self._outputList.Append(errorMessage)
                        else:
                            self.AddedCryptoFile(cryptoObjName, objName, objSize, objTime)
                            expectedNextFileNumber = objNumber + 1
                            self.addFileToCurrentFolderList(objName, objSize, objTime)
                    else:
                        errorMessage = 'Found a WTF: cryptoObjName=' + cryptoObjName + ' objName=' + objName
                        DEBUG(errorMessage)
                        self._outputList.Append(errorMessage)

                self.cryptoDirNextFree = expectedNextDirNumber
                self.cryptoFileNextFree = expectedNextFileNumber

        except S3ResponseError as ex:
            if ex.error_message != None and len(ex.error_message) > 0:
                msg = ex.error_message
            else:
                msg = str(ex)
            errorMessage = 'S3 Error (getting encrypted dir contents): "' + msg + '"'
            DEBUG(errorMessage)
            self._outputList.Append(errorMessage)

        except Exception as ex:
            errorMessage = 'Error (getting encrypted dir contents): ' + str(ex)
            DEBUG(errorMessage)
            self._outputList.Append(errorMessage)



    def GetRemoteDirContents(self, bucket, bucketListPrefix):
        DEBUG('GetRemoteDirContents: bucket.name=' + bucket.name)
        DEBUG('GetRemoteDirContents: bucketListPrefix=' + bucketListPrefix)
        if bucketListPrefix == '/':
            bucketListPrefix = ''
        files = bucket.list(bucketListPrefix, '/')

        # Seems to have worked so update the folder location
        self.currentFolderFolders = []
        self.currentFolderFiles = []

        # List the folders and files
        for fileEntry in files:
            DEBUG('fileEntry.name=' + fileEntry.name)
            if fileEntry.name == bucketListPrefix + '.$folder$' or fileEntry.name == bucketListPrefix or fileEntry.name.startswith('_CF'):
                continue

            fullName = fileEntry.name[len(bucketListPrefix):]
            if fullName.endswith('/'):
                folderName = fullName.rstrip('/')
                self.currentFolderFolders.append((folderName, '', 'Folder', '', 18))

            else:
                t = time.strptime(fileEntry.last_modified[:19], '%Y-%m-%dT%H:%M:%S')
                timeStr = time.strftime('%Y-%m-%d %H:%M', t)
                self.addFileToCurrentFolderList(fullName, str(fileEntry.size), timeStr)


    def addFileToCurrentFolderList(self, fullName, fileSize, timeStr):
        if '.' in fullName:
            dotIdx = fullName.rfind('.')
            ext = fullName[dotIdx+1:]
        else:
            ext = ''

        ex = ext.lower()
        fileType, imageIndex = self.GetFileType(ex)
        self.currentFolderFiles.append((fullName, fileSize, fileType, timeStr, imageIndex))


    def DisplayRemoteFolder(self):
        DEBUG('Display folder: currentRemotePath=' + self._currentRemotePath)
        DEBUG('Display folder: currentRemoteCryptoPath=' + self._currentRemoteCryptoPath)

        self.DeleteAllItems()

        bucket, bucketListPrefix = self.GetCurrentRemoteLocation()
        if bucket is None:
            self.DisplayBucketList()
        else:
            self._outputList.Append('ls ' + self._currentRemotePath)
            DEBUG('List bucket: prefix = ' + bucketListPrefix)

            if len(self._password) > 0:
                self.GetEncryptedDirContents(bucket, bucketListPrefix)
            else:
                self.GetRemoteDirContents(bucket, bucketListPrefix)

            self.displayRoot = True
            self.ResetColumnSorting()
            self.DisplayFileList()
               

    def DisplayFileList(self):
        if self.sortColumn == SORT_BY_FILENAME:
            # sort by filename
            if self._sortAscending:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower())
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s1.lower())
            else:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower(), reverse=True)
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s1.lower(), reverse=True)
        elif self.sortColumn == SORT_BY_FILESIZE:
            # sort by filesize
            if self._sortAscending:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower())
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): int(s2))
            else:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower())
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): int(s2), reverse=True)
        elif self.sortColumn == SORT_BY_FILETYPE:
            # sort by filetype
            if self._sortAscending:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower())
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s3.lower())
            else:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s1.lower())
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s3.lower(), reverse=True)
        elif self.sortColumn == SORT_BY_MODIFIED:
            # sort by modification timestamp
            if self._sortAscending:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s4)
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s4)
            else:
                self.currentFolderFolders = sorted(self.currentFolderFolders, key=lambda (s1, s2, s3, s4, i1): s4, reverse=True)
                self.currentFolderFiles = sorted(self.currentFolderFiles, key=lambda (s1, s2, s3, s4, i1): s4, reverse=True)

        if self.displayRoot:
            index = self.InsertStringItem(sys.maxint, '..')
            self.SetStringItem(index, 2, 'Folder')
            self.SetItemImage(index, 0)

        for details in self.currentFolderFolders:
            index = self.InsertStringItem(sys.maxint, details[0])
            self.SetStringItem(index, 1, details[1])
            self.SetStringItem(index, 2, details[2])
            self.SetStringItem(index, 3, details[3])
            self.SetItemImage(index, details[4])

        for details in self.currentFolderFiles:
            index = self.InsertStringItem(sys.maxint, details[0])
            self.SetStringItem(index, 1, details[1])
            self.SetStringItem(index, 2, details[2])
            self.SetStringItem(index, 3, details[3])
            self.SetItemImage(index, details[4])


class OutputList(wx.ListBox):
    
    def __init__(self, *args, **kwargs):
        super(OutputList, self).__init__(*args, **kwargs) 
            
        self.InitUI()

        
    def InitUI(self):
        self.SetFont(wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)


    def Append(self, msg):
        rows = msg.split('\n')
        for row in rows:
            index = wx.ListBox.Append(self, row)
        self.SetFirstItem(index)


    def OnRightDown(self, e):
        self.outputListMenu = wx.Menu()
        mi = wx.MenuItem(self.outputListMenu, wx.NewId(), 'Select All')
        self.outputListMenu.AppendItem(mi)
        self.Bind(wx.EVT_MENU, self.OnOutputListSelectAll, mi)
        mi = wx.MenuItem(self.outputListMenu, wx.NewId(), 'Copy')
        self.outputListMenu.AppendItem(mi)
        self.Bind(wx.EVT_MENU, self.OnOutputListCopy, mi)

        self.PopupMenu(self.outputListMenu, e.GetPosition())


    def OnOutputListSelectAll(self, e):
        for i in range(self.GetCount()):
            self.SetSelection(i)


    def OnOutputListCopy(self, e):
        text = ''
        selections = self.GetSelections()
        for i in selections:
            text = text + self.GetString(i) + '\n'

        if wx.TheClipboard.Open():
            wx.TheClipboard.Clear()
            wx.TheClipboard.SetData(wx.TextDataObject(text))
            wx.TheClipboard.Close()



class MainForm(wx.Frame):
    
    def __init__(self, *args, **kwargs):
        super(MainForm, self).__init__(*args, **kwargs) 
            
        self.accessKey = None
        self.secretKey = None
        self.password = None
        
        set_main_dir() # Find out where we are installed so we can pick up stuff like icon images

        AppConfig.Init(get_main_dir()) # Load the configuration settings from $HOME/CloudFiler.ini 

        self.InitUI()

        global gbl_newerVersionAvailable
        gbl_newerVersionAvailable = False
        if self.verInfoCheck:
            thread = VersionCheckThread()
            thread.start()

        
    def InitUI(self):

        self.icon = wx.Icon(GetFilePath(ICON_APPLICATION), wx.BITMAP_TYPE_ICO)
        self.SetIcon(self.icon)
        
#        icon = wx.IconFromBitmap(wx.Bitmap(GetIconPath(ICON_REMOTE_FOLDER)))
#        self.SetIcon(icon)#, TRAY_TOOLTIP)

#        image = wx.Image(GetIconPath(ICON_APPLICATION), wx.BITMAP_TYPE_PNG).ConvertToBitmap()
#        icon = wx.EmptyIcon()
#        icon.CopyFromBitmap(image)
#        self.SetIcon(icon)

#        ib = wx.IconBundle()
#        ib.AddIconFromFile(GetIconPath(ICON_REMOTE_FOLDER), wx.BITMAP_TYPE_PNG)
#        self.SetIcons(ib)
        
        menubar = wx.MenuBar()

        fileMenu = wx.Menu()
        fileMenu.Append(wx.ID_OPEN, '&Open')
        fileMenu.Append(ID_REMOTE_CONNECT, '&Connect')
        fileMenu.AppendSeparator()

        qmi = wx.MenuItem(fileMenu, wx.ID_EXIT, '&Quit\tCtrl+W')
        fileMenu.AppendItem(qmi)

        self.Bind(wx.EVT_MENU, self.OnQuit, qmi)

        transferMenu = wx.Menu()
        transferMenu.Append(ID_LOCAL_UPLOAD, 'Upload')
        transferMenu.Append(ID_REMOTE_DOWNLOAD, 'Download')

        helpMenu = wx.Menu()
        helpMenu.Append(ID_OPEN_HELP, 'User Guide')
#        helpMenu.Append(ID_OPEN_HELP_LICENSING, 'Licensing')
        helpMenu.Append(ID_OPEN_HELP_ABOUT, 'About')

        menubar.Append(fileMenu, '&File')
        menubar.Append(transferMenu, '&Transfer')
        menubar.Append(helpMenu, '&Help')
        self.SetMenuBar(menubar)

        toolbar = self.CreateToolBar()
        toolbar.SetBackgroundColour('#E0E0E0')
        qtool = toolbar.AddLabelTool(wx.ID_OPEN, 'Open', wx.Bitmap(GetFilePath(ICON_FOLDER)), shortHelp='Open')
        qtool = toolbar.AddLabelTool(ID_REMOTE_CONNECT, 'Connect', wx.Bitmap(GetFilePath(ICON_REMOTE_FOLDER)), shortHelp='Connect')
        toolbar.AddSeparator()
        qtool = toolbar.AddLabelTool(ID_OPEN_HELP, 'Help', wx.Bitmap(GetFilePath(ICON_HELP_CONTENTS)), shortHelp='Help')
        qtool = toolbar.AddLabelTool(ID_OPEN_HELP_ABOUT, 'About', wx.Bitmap(GetFilePath(ICON_HELP_ABOUT)), shortHelp='About')
        toolbar.Realize()

        # Create the outer horizontal splitter with a vertical splitter above
        # and the output text box below

        hsplitter = wx.SplitterWindow(self, -1)

        # Create the top half of the horizontal splitter
        vsplitter = wx.SplitterWindow(hsplitter, -1)

        # Create the top left
        topLeftPanel = wx.Panel(vsplitter, -1)
        topLeftPanel.SetBackgroundColour('#E0E0E0')
        topRightPanel = wx.Panel(vsplitter, -1)
        topRightPanel.SetBackgroundColour('#E0E0E0')

        self.localDirCombo = wx.ComboBox(topLeftPanel, ID_LOCAL_DIR_COMBO, style=wx.TE_PROCESS_ENTER)
#        self.localDirCombo.SetStringSelection(homeDirPath)

        self.localFileList = FileListCtrl(topLeftPanel, -1, style=wx.LC_REPORT)

        topLeftSizer = wx.BoxSizer(wx.VERTICAL)
        topLeftSizer.Add(self.localDirCombo, 0, wx.EXPAND, 1)
        topLeftSizer.Add(self.localFileList, 1, wx.EXPAND | wx.ALL, 1)
        topLeftPanel.SetSizer(topLeftSizer)


        # Create the top right
        self.remoteIcon = wx.StaticBitmap(topRightPanel, bitmap=wx.Bitmap(GetFilePath(ICON_RED_SHIELD)), size=(16,16))
        self.remoteDirCombo = wx.ComboBox(topRightPanel, ID_REMOTE_DIR_COMBO, style=wx.TE_PROCESS_ENTER|wx.TE_READONLY)

        topRightIconComboSizer = wx.BoxSizer(wx.HORIZONTAL)
        topRightIconComboSizer.Add(self.remoteIcon, 0, wx.EXPAND|wx.LEFT|wx.TOP, border=4)
        topRightIconComboSizer.Add(self.remoteDirCombo, 1, wx.EXPAND|wx.LEFT, border=4)

        self.remoteFileList = FileListCtrl(topRightPanel, -1, style=wx.LC_REPORT)
#        self.remoteFileList.SetBackgroundColour(wx.Colour(200,240,200))

        topRightSizer = wx.BoxSizer(wx.VERTICAL)
        topRightSizer.Add(topRightIconComboSizer, 0, wx.EXPAND, 1)
        topRightSizer.Add(self.remoteFileList, 1, wx.EXPAND | wx.ALL, 1)
        topRightPanel.SetSizer(topRightSizer)

        vsplitter.SplitVertically(topLeftPanel, topRightPanel)
        

        # Create the bottom half of the horizontal splitter
        bottomPanel = wx.Panel(hsplitter, -1)
        bottomPanel.SetBackgroundColour('#E0E0E0')

        self.outputList = OutputList(bottomPanel, style=wx.LB_EXTENDED)

        bottomSizer = wx.BoxSizer(wx.HORIZONTAL)
        bottomSizer.Add(self.outputList, 1, wx.EXPAND | wx.ALL, 1)

        bottomPanel.SetSizer(bottomSizer)

        hsplitter.SplitHorizontally(vsplitter, bottomPanel)

        # Setup events
        self.Bind(wx.EVT_TEXT_ENTER, self.OnLocalDirText, id=ID_LOCAL_DIR_COMBO)
        self.Bind(wx.EVT_COMBOBOX, self.OnLocalDirText, id=ID_LOCAL_DIR_COMBO)
        self.Bind(wx.EVT_MENU, self.OnBrowseLocal, id=wx.ID_OPEN)
        self.Bind(wx.EVT_TEXT_ENTER, self.OnRemoteDirText, id=ID_REMOTE_DIR_COMBO)
        self.Bind(wx.EVT_COMBOBOX, self.OnRemoteDirText, id=ID_REMOTE_DIR_COMBO)
        self.Bind(wx.EVT_MENU, self.OnBrowseRemote, id=ID_REMOTE_CONNECT)
        self.Bind(wx.EVT_MENU, self.OnOpenHelp, id=ID_OPEN_HELP)
        self.Bind(wx.EVT_MENU, self.OnOpenHelpAbout, id=ID_OPEN_HELP_ABOUT)
#        self.Bind(wx.EVT_MENU, self.OnOpenHelpLicensing, id=ID_OPEN_HELP_LICENSING)

        self.Bind(wx.EVT_MENU, self.OnLocalUpload, id=ID_LOCAL_UPLOAD)
        self.Bind(wx.EVT_MENU, self.OnRemoteDownload, id=ID_REMOTE_DOWNLOAD)

        try:
            if AppConfig.Get('History', 'UserSupressedVersionCheck') == 'Y':
                self.verInfoCheck = False
            else:
                self.verInfoCheck = True
        except:
            self.verInfoCheck = True
        if self.verInfoCheck:
            self.Bind(wx.EVT_IDLE, self.OnIdle)

        # Initialise licensing
        try:
            key = AppConfig.Get('History', 'LicenseKey')
            DEBUG('LicenseKey=' + key)
        except:
            key = ''
        Licensing.Init(key)

        # Open initial local directory
        homeDirPath = os.path.expanduser("~")
        localDirList = [homeDirPath]
        self.localFileList.Init(None, self.localDirCombo, self.outputList, self.remoteFileList, None)
        self.localFileList.SetDir(homeDirPath)
        self.localDirCombo.Append(homeDirPath)
        self.localDirCombo.SetStringSelection(homeDirPath)

        # Initialise the remote directory list control
        self.remoteFileList.Init(self.remoteIcon, self.remoteDirCombo, self.outputList, None, self.localFileList)

        self.SetSize((1200, 800))
        self.SetTitle('Cloud Filer')
        self.Centre()
#        self.Show(True)
        

    def OnIdle(self, e):
        global gbl_newerVersionAvailable
        if gbl_newerVersionAvailable:
            gbl_newerVersionAvailable = False
            self.Unbind(wx.EVT_IDLE, handler=self.OnIdle)
            dlg = InformationDlg(ACTION_NEW_VERSION, False, self, wx.ID_ANY, 'Cloud Filer - Information')
            dlg.ShowModal()
            if dlg.GetEnoughAlready():
                AppConfig.Set('History', 'UserSupressedVersionCheck', 'Y')
                AppConfig.WriteSettings()
            dlg.Destroy()


    def OnLocalUpload(self, e):
        self.localFileList.OnUpload(None)


    def OnRemoteDownload(self, e):
        self.remoteFileList.OnDownload(None)


    def OnOpenHelp(self, e):
        DEBUG('OnOpenHelp: os.name=' + os.name)
        try:
            if GetOS() == ID_OS_WINDOWS:
                os.startfile(GetFilePath(USER_GUIDE_APPLICATION)) # Windows
            elif GetOS() == ID_OS_LINUX:
                os.system("xdg-open "+GetFilePath(USER_GUIDE_APPLICATION)) # Linux
            elif GetOS() == ID_OS_MACOS:
                os.system('open', filepath) # MacOS
            else:
                DEBUG('Unexpected OS')
        except:
            DEBUG('Open help exception (is a PDF reader installed?)')
            pass


    def OnOpenHelpLicensing(self, e):
        if not Licensing.EncryptionLicensed():
            dlg = GetNewLicenseDlg(Licensing.m_licensingMsg, self, wx.ID_ANY, 'License Encryption Module')
            if dlg.ShowModal() == wx.ID_OK:
                self.outputList.Append('Activating license...')
                key = dlg.GetKey()
                Licensing.Init(key)
                if (Licensing.EncryptionLicensed()):
                    self.outputList.Append('License retireved...')
                    AppConfig.Set('History', 'LicenseKey', key)
                    AppConfig.WriteSettings()
                    self.outputList.Append('Licensed ok')
                else:
                    self.outputList.Append('Failed to activate license: ' + Licensing.m_licensingMsg)

            dlg.Destroy()
        else:
            dlg = InformationDlg(ACTION_LICENSE_DETAILS, False, self, wx.ID_ANY, 'Cloud Filer - License Details')
            dlg.ShowModal()
            dlg.Destroy()


    def OnOpenHelpAbout(self, e):
        
        description = """
Cloud Filer is a cloud storage file management client.
Cloud Filer allows you to upload and download files to
cloud storage and can be used as part of a backup strategy.
For information about updates check out the website.
"""

        licence = """
The MIT License (MIT)

Copyright (c) 2014 Nuvovis

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
        info = wx.AboutDialogInfo()

        info.SetName('Cloud Filer')
        info.SetVersion(CF_VERSION_STRING)
        info.SetDescription(description)
        info.SetCopyright('(C) 2014 Nuvovis')
        info.SetWebSite('http://www.nuvovis.com')
        info.SetLicence(licence)

        wx.AboutBox(info)


    def OnLocalDirText(self, e):
        newDir = self.localDirCombo.GetValue()
        if os.path.isdir(newDir):
            self.localFileList.SetDir(newDir)
        else:
            self.outputList.Append('Invalid dir: ' + newDir)


    def OnRemoteDirText(self, e):
        newDir = self.remoteDirCombo.GetValue()
        self.remoteFileList.SetRemoteCryptoDirFromCache(newDir)
        self.remoteFileList.ChangeRemoteDir(newDir)


    def OnBrowseLocal(self, e):
        dlg = wx.DirDialog(self, 'Select a local folder', style=wx.DD_CHANGE_DIR|wx.DD_DIR_MUST_EXIST)
        if dlg.ShowModal() == wx.ID_OK:
            self.localDirCombo.Append(dlg.GetPath())
            self.localDirCombo.SetStringSelection(dlg.GetPath())
            self.localFileList.SetDir(dlg.GetPath())

        dlg.Destroy()


    def OnBrowseRemote(self, e):
        if self.GetCloudCredentials():
            self.remoteFileList.S3Connect(self.accessKey, self.secretKey, self.password, self._cloudService, self._cloudHost, self._cloudRootText)
            if self.remoteFileList._connected:
                self.remoteFileList.ChangeRemoteDir(self._cloudRootText)


    def GetCloudCredentials(self):
        dlg = CloudConnectDlg(self, wx.ID_ANY, 'Specify cloud connection')
        if dlg.ShowModal() == wx.ID_OK:
            self.accessKey = dlg.GetAccessKey()
            self.secretKey = dlg.GetSecretKey()
            self.password = dlg.GetPassword()
            self._cloudService = dlg.GetCloudService()
            self._cloudHost = dlg.GetCloudHost()
            self._cloudRootText = dlg.GetRootText()
            keyKey = 'nuvovis.com CloudFiler ' + self._cloudRootText.rstrip(':')
            import unicodedata
            keyKey = unicodedata.normalize('NFKD', keyKey).encode('ascii', 'ignore')
            if dlg.StoreKeys():
                AppConfig.SecureSet(keyKey, 'AK', self.accessKey)
                AppConfig.SecureSet(keyKey, 'SK', self.secretKey)
            else:
                AppConfig.SecureSet(keyKey, 'AK', '')
                AppConfig.SecureSet(keyKey, 'SK', '')
            
            if dlg.StorePassword():
                AppConfig.SecureSet(keyKey, 'PWD', self.password)
            else:
                AppConfig.SecureSet(keyKey, 'PWD', '')

            if not dlg.GetEncrypt():
                self.password = '' # Switch off encryption now that we have saved the password if necessary

            dlg.SaveLastConnection() 
# With later keyring can use delete_password
# keyring.delete_password('AWSS3', 'AK')
# keyring.delete_password('AWSS3', 'SK')

#            self.remoteFileList.SetRemoteDir(self._cloudRootText)
            hitOk = True

        else:
            hitOk = False

        dlg.Destroy()
        return hitOk

    def OnQuit(self, e):
        self.Close()



def main():
    app = wx.App(False)  # Create a new app, don't redirect stdout/stderr to a window.
    frame = MainForm(None)
    frame.Show(True)     # Show the frame.
    app.MainLoop()


if __name__ == '__main__':
    main()

